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