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