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 << "Members contains " << itMembers->size() 477 << " element, returning full collection."; 478 completionHandler(res); 479 return false; 480 } 481 482 auto itUrl = itMemBegin->find("@odata.id"); 483 if (itUrl == itMemBegin->end()) 484 { 485 BMCWEB_LOG_DEBUG << "No found odata.id"; 486 messages::internalError(res); 487 completionHandler(res); 488 return false; 489 } 490 const std::string* url = itUrl->get_ptr<const std::string*>(); 491 if (url == nullptr) 492 { 493 BMCWEB_LOG_DEBUG << "@odata.id wasn't a string????"; 494 messages::internalError(res); 495 completionHandler(res); 496 return false; 497 } 498 // TODO(Ed) copy request headers? 499 // newReq.session = req.session; 500 std::error_code ec; 501 crow::Request newReq({boost::beast::http::verb::get, *url, 11}, ec); 502 if (ec) 503 { 504 messages::internalError(res); 505 completionHandler(res); 506 return false; 507 } 508 509 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 510 BMCWEB_LOG_DEBUG << "setting completion handler on " << &asyncResp->res; 511 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); 512 asyncResp->res.setIsAliveHelper(res.releaseIsAliveHelper()); 513 app.handle(newReq, asyncResp); 514 return true; 515 } 516 517 struct ExpandNode 518 { 519 nlohmann::json::json_pointer location; 520 std::string uri; 521 522 inline bool operator==(const ExpandNode& other) const 523 { 524 return location == other.location && uri == other.uri; 525 } 526 }; 527 528 inline void findNavigationReferencesInArrayRecursive( 529 ExpandType eType, nlohmann::json::array_t& array, 530 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth, 531 bool inLinks, std::vector<ExpandNode>& out); 532 533 inline void findNavigationReferencesInObjectRecursive( 534 ExpandType eType, nlohmann::json::object_t& obj, 535 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth, 536 bool inLinks, std::vector<ExpandNode>& out); 537 538 // Walks a json object looking for Redfish NavigationReference entries that 539 // might need resolved. It recursively walks the jsonResponse object, looking 540 // for links at every level, and returns a list (out) of locations within the 541 // tree that need to be expanded. The current json pointer location p is passed 542 // in to reference the current node that's being expanded, so it can be combined 543 // with the keys from the jsonResponse object 544 inline void findNavigationReferencesRecursive( 545 ExpandType eType, nlohmann::json& jsonResponse, 546 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth, 547 bool inLinks, std::vector<ExpandNode>& out) 548 { 549 // If no expand is needed, return early 550 if (eType == ExpandType::None) 551 { 552 return; 553 } 554 555 nlohmann::json::array_t* array = 556 jsonResponse.get_ptr<nlohmann::json::array_t*>(); 557 if (array != nullptr) 558 { 559 findNavigationReferencesInArrayRecursive(eType, *array, jsonPtr, depth, 560 skipDepth, inLinks, out); 561 } 562 nlohmann::json::object_t* obj = 563 jsonResponse.get_ptr<nlohmann::json::object_t*>(); 564 if (obj == nullptr) 565 { 566 return; 567 } 568 findNavigationReferencesInObjectRecursive(eType, *obj, jsonPtr, depth, 569 skipDepth, inLinks, out); 570 } 571 572 inline void findNavigationReferencesInArrayRecursive( 573 ExpandType eType, nlohmann::json::array_t& array, 574 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth, 575 bool inLinks, std::vector<ExpandNode>& out) 576 { 577 size_t index = 0; 578 // For arrays, walk every element in the array 579 for (auto& element : array) 580 { 581 nlohmann::json::json_pointer newPtr = jsonPtr / index; 582 BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr.to_string(); 583 findNavigationReferencesRecursive(eType, element, newPtr, depth, 584 skipDepth, inLinks, out); 585 index++; 586 } 587 } 588 589 inline void findNavigationReferencesInObjectRecursive( 590 ExpandType eType, nlohmann::json::object_t& obj, 591 const nlohmann::json::json_pointer& jsonPtr, int depth, int skipDepth, 592 bool inLinks, std::vector<ExpandNode>& out) 593 { 594 // Navigation References only ever have a single element 595 if (obj.size() == 1) 596 { 597 if (obj.begin()->first == "@odata.id") 598 { 599 const std::string* uri = 600 obj.begin()->second.get_ptr<const std::string*>(); 601 if (uri != nullptr) 602 { 603 BMCWEB_LOG_DEBUG << "Found " << *uri << " at " 604 << 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 campatible 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 // Propogates 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 explictly) 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 // Higer 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 // Propogates 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.size() << " nodes to traverse"; 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 << &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::find(reservedProperties.begin(), 963 reservedProperties.end(), 964 it.key()) != reservedProperties.end(); 965 if (reserved || (nextNode != nullptr && nextNode->isSelected())) 966 { 967 it = nextIt; 968 continue; 969 } 970 if (nextNode != nullptr) 971 { 972 BMCWEB_LOG_DEBUG << "Recursively select: " << it.key(); 973 recursiveSelect(*it, *nextNode); 974 it = nextIt; 975 continue; 976 } 977 BMCWEB_LOG_DEBUG << it.key() << " is getting removed!"; 978 it = currRoot.erase(it); 979 } 980 } 981 nlohmann::json::array_t* array = 982 currRoot.get_ptr<nlohmann::json::array_t*>(); 983 if (array != nullptr) 984 { 985 BMCWEB_LOG_DEBUG << "Current JSON is an array"; 986 // Array index is omitted, so reuse the same Trie node 987 for (nlohmann::json& nextRoot : *array) 988 { 989 recursiveSelect(nextRoot, currNode); 990 } 991 } 992 } 993 994 // The current implementation of $select still has the following TODOs due to 995 // ambiguity and/or complexity. 996 // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was 997 // created for clarification. 998 // 2. respect the full odata spec; e.g., deduplication, namespace, star (*), 999 // etc. 1000 inline void processSelect(crow::Response& intermediateResponse, 1001 const SelectTrieNode& trieRoot) 1002 { 1003 BMCWEB_LOG_DEBUG << "Process $select quary parameter"; 1004 recursiveSelect(intermediateResponse.jsonValue, trieRoot); 1005 } 1006 1007 inline void 1008 processAllParams(crow::App& app, const Query& query, const Query& delegated, 1009 std::function<void(crow::Response&)>& completionHandler, 1010 crow::Response& intermediateResponse) 1011 { 1012 if (!completionHandler) 1013 { 1014 BMCWEB_LOG_DEBUG << "Function was invalid?"; 1015 return; 1016 } 1017 1018 BMCWEB_LOG_DEBUG << "Processing query params"; 1019 // If the request failed, there's no reason to even try to run query 1020 // params. 1021 if (intermediateResponse.resultInt() < 200 || 1022 intermediateResponse.resultInt() >= 400) 1023 { 1024 completionHandler(intermediateResponse); 1025 return; 1026 } 1027 if (query.isOnly) 1028 { 1029 processOnly(app, intermediateResponse, completionHandler); 1030 return; 1031 } 1032 1033 if (query.top || query.skip) 1034 { 1035 processTopAndSkip(query, intermediateResponse); 1036 } 1037 1038 if (query.expandType != ExpandType::None) 1039 { 1040 BMCWEB_LOG_DEBUG << "Executing expand query"; 1041 auto asyncResp = std::make_shared<bmcweb::AsyncResp>( 1042 std::move(intermediateResponse)); 1043 1044 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); 1045 auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp); 1046 multi->startQuery(query, delegated); 1047 return; 1048 } 1049 1050 // According to Redfish Spec Section 7.3.1, $select is the last parameter to 1051 // to process 1052 if (!query.selectTrie.root.empty()) 1053 { 1054 processSelect(intermediateResponse, query.selectTrie.root); 1055 } 1056 1057 completionHandler(intermediateResponse); 1058 } 1059 1060 } // namespace query_param 1061 } // namespace redfish 1062