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