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