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& p, 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& p, 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& p, 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, p, 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, p, depth, skipDepth, 569 inLinks, out); 570 } 571 572 inline void findNavigationReferencesInArrayRecursive( 573 ExpandType eType, nlohmann::json::array_t* array, 574 const nlohmann::json::json_pointer& p, 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 = p / 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& p, 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 " << p.to_string(); 604 if (skipDepth == 0) 605 { 606 out.push_back({p, *uri}); 607 } 608 return; 609 } 610 } 611 } 612 613 int newDepth = depth; 614 auto odataId = obj->find("@odata.id"); 615 if (odataId != obj->end()) 616 { 617 // The Redfish spec requires all resources to include the resource 618 // identifier. If the object has multiple elements and one of them is 619 // "@odata.id" then that means we have entered a new level / expanded 620 // resource. We need to stop traversing if we're already at the desired 621 // depth 622 if (obj->size() > 1) 623 { 624 if (depth == 0) 625 { 626 return; 627 } 628 if (skipDepth > 0) 629 { 630 skipDepth--; 631 } 632 } 633 634 if (skipDepth == 0) 635 { 636 newDepth--; 637 } 638 } 639 640 // Loop the object and look for links 641 for (auto& element : *obj) 642 { 643 bool localInLinks = inLinks; 644 if (!localInLinks) 645 { 646 // Check if this is a links node 647 localInLinks = element.first == "Links"; 648 } 649 // Only traverse the parts of the tree the user asked for 650 // Per section 7.3 of the redfish specification 651 if (localInLinks && eType == ExpandType::NotLinks) 652 { 653 continue; 654 } 655 if (!localInLinks && eType == ExpandType::Links) 656 { 657 continue; 658 } 659 nlohmann::json::json_pointer newPtr = p / element.first; 660 BMCWEB_LOG_DEBUG << "Traversing response at " << newPtr; 661 662 findNavigationReferencesRecursive(eType, element.second, newPtr, 663 newDepth, skipDepth, localInLinks, 664 out); 665 } 666 } 667 668 // TODO: When aggregation is enabled and we receive a partially expanded 669 // response we may need need additional handling when the original URI was 670 // up tree from a top level collection. 671 // Isn't a concern until https://gerrit.openbmc.org/c/openbmc/bmcweb/+/60556 672 // lands. May want to avoid forwarding query params when request is uptree from 673 // a top level collection. 674 inline std::vector<ExpandNode> 675 findNavigationReferences(ExpandType eType, int depth, int skipDepth, 676 nlohmann::json& jsonResponse) 677 { 678 std::vector<ExpandNode> ret; 679 const nlohmann::json::json_pointer root = nlohmann::json::json_pointer(""); 680 // SkipDepth +1 since we are skipping the root by default. 681 findNavigationReferencesRecursive(eType, jsonResponse, root, depth, 682 skipDepth + 1, false, ret); 683 return ret; 684 } 685 686 // Formats a query parameter string for the sub-query. 687 // Returns std::nullopt on failures. 688 // This function shall handle $select when it is added. 689 // There is no need to handle parameters that's not campatible with $expand, 690 // e.g., $only, since this function will only be called in side $expand handlers 691 inline std::optional<std::string> formatQueryForExpand(const Query& query) 692 { 693 // query.expandLevel<=1: no need to do subqueries 694 if (query.expandLevel <= 1) 695 { 696 return ""; 697 } 698 std::string str = "?$expand="; 699 bool queryTypeExpected = false; 700 switch (query.expandType) 701 { 702 case ExpandType::None: 703 return ""; 704 case ExpandType::Links: 705 queryTypeExpected = true; 706 str += '~'; 707 break; 708 case ExpandType::NotLinks: 709 queryTypeExpected = true; 710 str += '.'; 711 break; 712 case ExpandType::Both: 713 queryTypeExpected = true; 714 str += '*'; 715 break; 716 } 717 if (!queryTypeExpected) 718 { 719 return std::nullopt; 720 } 721 str += "($levels="; 722 str += std::to_string(query.expandLevel - 1); 723 str += ')'; 724 return str; 725 } 726 727 // Propogates the worst error code to the final response. 728 // The order of error code is (from high to low) 729 // 500 Internal Server Error 730 // 511 Network Authentication Required 731 // 510 Not Extended 732 // 508 Loop Detected 733 // 507 Insufficient Storage 734 // 506 Variant Also Negotiates 735 // 505 HTTP Version Not Supported 736 // 504 Gateway Timeout 737 // 503 Service Unavailable 738 // 502 Bad Gateway 739 // 501 Not Implemented 740 // 401 Unauthorized 741 // 451 - 409 Error codes (not listed explictly) 742 // 408 Request Timeout 743 // 407 Proxy Authentication Required 744 // 406 Not Acceptable 745 // 405 Method Not Allowed 746 // 404 Not Found 747 // 403 Forbidden 748 // 402 Payment Required 749 // 400 Bad Request 750 inline unsigned propogateErrorCode(unsigned finalCode, unsigned subResponseCode) 751 { 752 // We keep a explicit list for error codes that this project often uses 753 // Higer priority codes are in lower indexes 754 constexpr std::array<unsigned, 13> orderedCodes = { 755 500, 507, 503, 502, 501, 401, 412, 409, 406, 405, 404, 403, 400}; 756 size_t finalCodeIndex = std::numeric_limits<size_t>::max(); 757 size_t subResponseCodeIndex = std::numeric_limits<size_t>::max(); 758 for (size_t i = 0; i < orderedCodes.size(); ++i) 759 { 760 if (orderedCodes[i] == finalCode) 761 { 762 finalCodeIndex = i; 763 } 764 if (orderedCodes[i] == subResponseCode) 765 { 766 subResponseCodeIndex = i; 767 } 768 } 769 if (finalCodeIndex != std::numeric_limits<size_t>::max() && 770 subResponseCodeIndex != std::numeric_limits<size_t>::max()) 771 { 772 return finalCodeIndex <= subResponseCodeIndex ? finalCode 773 : subResponseCode; 774 } 775 if (subResponseCode == 500 || finalCode == 500) 776 { 777 return 500; 778 } 779 if (subResponseCode > 500 || finalCode > 500) 780 { 781 return std::max(finalCode, subResponseCode); 782 } 783 if (subResponseCode == 401) 784 { 785 return subResponseCode; 786 } 787 return std::max(finalCode, subResponseCode); 788 } 789 790 // Propogates all error messages into |finalResponse| 791 inline void propogateError(crow::Response& finalResponse, 792 crow::Response& subResponse) 793 { 794 // no errors 795 if (subResponse.resultInt() >= 200 && subResponse.resultInt() < 400) 796 { 797 return; 798 } 799 messages::moveErrorsToErrorJson(finalResponse.jsonValue, 800 subResponse.jsonValue); 801 finalResponse.result( 802 propogateErrorCode(finalResponse.resultInt(), subResponse.resultInt())); 803 } 804 805 class MultiAsyncResp : public std::enable_shared_from_this<MultiAsyncResp> 806 { 807 public: 808 // This object takes a single asyncResp object as the "final" one, then 809 // allows callers to attach sub-responses within the json tree that need 810 // to be executed and filled into their appropriate locations. This 811 // class manages the final "merge" of the json resources. 812 MultiAsyncResp(crow::App& appIn, 813 std::shared_ptr<bmcweb::AsyncResp> finalResIn) : 814 app(appIn), 815 finalRes(std::move(finalResIn)) 816 {} 817 818 void addAwaitingResponse( 819 const std::shared_ptr<bmcweb::AsyncResp>& res, 820 const nlohmann::json::json_pointer& finalExpandLocation) 821 { 822 res->res.setCompleteRequestHandler(std::bind_front( 823 placeResultStatic, shared_from_this(), finalExpandLocation)); 824 } 825 826 void placeResult(const nlohmann::json::json_pointer& locationToPlace, 827 crow::Response& res) 828 { 829 BMCWEB_LOG_DEBUG << "placeResult for " << locationToPlace; 830 propogateError(finalRes->res, res); 831 if (!res.jsonValue.is_object() || res.jsonValue.empty()) 832 { 833 return; 834 } 835 nlohmann::json& finalObj = finalRes->res.jsonValue[locationToPlace]; 836 finalObj = std::move(res.jsonValue); 837 } 838 839 // Handles the very first level of Expand, and starts a chain of sub-queries 840 // for deeper levels. 841 void startQuery(const Query& query, const Query& delegated) 842 { 843 std::vector<ExpandNode> nodes = findNavigationReferences( 844 query.expandType, query.expandLevel, delegated.expandLevel, 845 finalRes->res.jsonValue); 846 BMCWEB_LOG_DEBUG << nodes.size() << " nodes to traverse"; 847 const std::optional<std::string> queryStr = formatQueryForExpand(query); 848 if (!queryStr) 849 { 850 messages::internalError(finalRes->res); 851 return; 852 } 853 for (const ExpandNode& node : nodes) 854 { 855 const std::string subQuery = node.uri + *queryStr; 856 BMCWEB_LOG_DEBUG << "URL of subquery: " << subQuery; 857 std::error_code ec; 858 crow::Request newReq({boost::beast::http::verb::get, subQuery, 11}, 859 ec); 860 if (ec) 861 { 862 messages::internalError(finalRes->res); 863 return; 864 } 865 866 auto asyncResp = std::make_shared<bmcweb::AsyncResp>(); 867 BMCWEB_LOG_DEBUG << "setting completion handler on " 868 << &asyncResp->res; 869 870 addAwaitingResponse(asyncResp, node.location); 871 app.handle(newReq, asyncResp); 872 } 873 } 874 875 private: 876 static void 877 placeResultStatic(const std::shared_ptr<MultiAsyncResp>& multi, 878 const nlohmann::json::json_pointer& locationToPlace, 879 crow::Response& res) 880 { 881 multi->placeResult(locationToPlace, res); 882 } 883 884 crow::App& app; 885 std::shared_ptr<bmcweb::AsyncResp> finalRes; 886 }; 887 888 inline void processTopAndSkip(const Query& query, crow::Response& res) 889 { 890 if (!query.skip && !query.top) 891 { 892 // No work to do. 893 return; 894 } 895 nlohmann::json::object_t* obj = 896 res.jsonValue.get_ptr<nlohmann::json::object_t*>(); 897 if (obj == nullptr) 898 { 899 // Shouldn't be possible. All responses should be objects. 900 messages::internalError(res); 901 return; 902 } 903 904 BMCWEB_LOG_DEBUG << "Handling top/skip"; 905 nlohmann::json::object_t::iterator members = obj->find("Members"); 906 if (members == obj->end()) 907 { 908 // From the Redfish specification 7.3.1 909 // ... the HTTP 400 Bad Request status code with the 910 // QueryNotSupportedOnResource message from the Base Message Registry 911 // for any supported query parameters that apply only to resource 912 // collections but are used on singular resources. 913 messages::queryNotSupportedOnResource(res); 914 return; 915 } 916 917 nlohmann::json::array_t* arr = 918 members->second.get_ptr<nlohmann::json::array_t*>(); 919 if (arr == nullptr) 920 { 921 messages::internalError(res); 922 return; 923 } 924 925 if (query.skip) 926 { 927 // Per section 7.3.1 of the Redfish specification, $skip is run before 928 // $top Can only skip as many values as we have 929 size_t skip = std::min(arr->size(), *query.skip); 930 arr->erase(arr->begin(), arr->begin() + static_cast<ssize_t>(skip)); 931 } 932 if (query.top) 933 { 934 size_t top = std::min(arr->size(), *query.top); 935 arr->erase(arr->begin() + static_cast<ssize_t>(top), arr->end()); 936 } 937 } 938 939 // Given a JSON subtree |currRoot|, this function erases leaves whose keys are 940 // not in the |currNode| Trie node. 941 inline void recursiveSelect(nlohmann::json& currRoot, 942 const SelectTrieNode& currNode) 943 { 944 nlohmann::json::object_t* object = 945 currRoot.get_ptr<nlohmann::json::object_t*>(); 946 if (object != nullptr) 947 { 948 BMCWEB_LOG_DEBUG << "Current JSON is an object"; 949 auto it = currRoot.begin(); 950 while (it != currRoot.end()) 951 { 952 auto nextIt = std::next(it); 953 BMCWEB_LOG_DEBUG << "key=" << it.key(); 954 const SelectTrieNode* nextNode = currNode.find(it.key()); 955 // Per the Redfish spec section 7.3.3, the service shall select 956 // certain properties as if $select was omitted. This applies to 957 // every TrieNode that contains leaves and the root. 958 constexpr std::array<std::string_view, 5> reservedProperties = { 959 "@odata.id", "@odata.type", "@odata.context", "@odata.etag", 960 "error"}; 961 bool reserved = std::find(reservedProperties.begin(), 962 reservedProperties.end(), 963 it.key()) != reservedProperties.end(); 964 if (reserved || (nextNode != nullptr && nextNode->isSelected())) 965 { 966 it = nextIt; 967 continue; 968 } 969 if (nextNode != nullptr) 970 { 971 BMCWEB_LOG_DEBUG << "Recursively select: " << it.key(); 972 recursiveSelect(*it, *nextNode); 973 it = nextIt; 974 continue; 975 } 976 BMCWEB_LOG_DEBUG << it.key() << " is getting removed!"; 977 it = currRoot.erase(it); 978 } 979 } 980 nlohmann::json::array_t* array = 981 currRoot.get_ptr<nlohmann::json::array_t*>(); 982 if (array != nullptr) 983 { 984 BMCWEB_LOG_DEBUG << "Current JSON is an array"; 985 // Array index is omitted, so reuse the same Trie node 986 for (nlohmann::json& nextRoot : *array) 987 { 988 recursiveSelect(nextRoot, currNode); 989 } 990 } 991 } 992 993 // The current implementation of $select still has the following TODOs due to 994 // ambiguity and/or complexity. 995 // 1. combined with $expand; https://github.com/DMTF/Redfish/issues/5058 was 996 // created for clarification. 997 // 2. respect the full odata spec; e.g., deduplication, namespace, star (*), 998 // etc. 999 inline void processSelect(crow::Response& intermediateResponse, 1000 const SelectTrieNode& trieRoot) 1001 { 1002 BMCWEB_LOG_DEBUG << "Process $select quary parameter"; 1003 recursiveSelect(intermediateResponse.jsonValue, trieRoot); 1004 } 1005 1006 inline void 1007 processAllParams(crow::App& app, const Query& query, const Query& delegated, 1008 std::function<void(crow::Response&)>& completionHandler, 1009 crow::Response& intermediateResponse) 1010 { 1011 if (!completionHandler) 1012 { 1013 BMCWEB_LOG_DEBUG << "Function was invalid?"; 1014 return; 1015 } 1016 1017 BMCWEB_LOG_DEBUG << "Processing query params"; 1018 // If the request failed, there's no reason to even try to run query 1019 // params. 1020 if (intermediateResponse.resultInt() < 200 || 1021 intermediateResponse.resultInt() >= 400) 1022 { 1023 completionHandler(intermediateResponse); 1024 return; 1025 } 1026 if (query.isOnly) 1027 { 1028 processOnly(app, intermediateResponse, completionHandler); 1029 return; 1030 } 1031 1032 if (query.top || query.skip) 1033 { 1034 processTopAndSkip(query, intermediateResponse); 1035 } 1036 1037 if (query.expandType != ExpandType::None) 1038 { 1039 BMCWEB_LOG_DEBUG << "Executing expand query"; 1040 auto asyncResp = std::make_shared<bmcweb::AsyncResp>( 1041 std::move(intermediateResponse)); 1042 1043 asyncResp->res.setCompleteRequestHandler(std::move(completionHandler)); 1044 auto multi = std::make_shared<MultiAsyncResp>(app, asyncResp); 1045 multi->startQuery(query, delegated); 1046 return; 1047 } 1048 1049 // According to Redfish Spec Section 7.3.1, $select is the last parameter to 1050 // to process 1051 if (!query.selectTrie.root.empty()) 1052 { 1053 processSelect(intermediateResponse, query.selectTrie.root); 1054 } 1055 1056 completionHandler(intermediateResponse); 1057 } 1058 1059 } // namespace query_param 1060 } // namespace redfish 1061