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