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