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