1 // SPDX-License-Identifier: Apache-2.0 2 // SPDX-FileCopyrightText: Copyright OpenBMC Authors 3 #pragma once 4 5 #include "aggregation_utils.hpp" 6 #include "async_resp.hpp" 7 #include "dbus_utility.hpp" 8 #include "error_messages.hpp" 9 #include "http_client.hpp" 10 #include "http_request.hpp" 11 #include "http_response.hpp" 12 #include "io_context_singleton.hpp" 13 #include "logging.hpp" 14 #include "parsing.hpp" 15 #include "ssl_key_handler.hpp" 16 #include "utility.hpp" 17 18 #include <boost/beast/http/field.hpp> 19 #include <boost/beast/http/status.hpp> 20 #include <boost/beast/http/verb.hpp> 21 #include <boost/system/errc.hpp> 22 #include <boost/system/result.hpp> 23 #include <boost/url/param.hpp> 24 #include <boost/url/parse.hpp> 25 #include <boost/url/segments_ref.hpp> 26 #include <boost/url/segments_view.hpp> 27 #include <boost/url/url.hpp> 28 #include <boost/url/url_view.hpp> 29 #include <nlohmann/json.hpp> 30 #include <sdbusplus/message/native_types.hpp> 31 32 #include <algorithm> 33 #include <array> 34 #include <chrono> 35 #include <cstddef> 36 #include <cstdint> 37 #include <functional> 38 #include <limits> 39 #include <memory> 40 #include <ranges> 41 #include <string> 42 #include <string_view> 43 #include <system_error> 44 #include <unordered_map> 45 #include <utility> 46 #include <variant> 47 48 namespace redfish 49 { 50 51 constexpr unsigned int aggregatorReadBodyLimit = 50 * 1024 * 1024; // 50MB 52 53 enum class Result 54 { 55 LocalHandle, 56 NoLocalHandle 57 }; 58 59 enum class SearchType 60 { 61 Collection, 62 CollOrCon, 63 ContainsSubordinate, 64 Resource 65 }; 66 67 // clang-format off 68 // These are all of the properties as of version 2022.2 of the Redfish Resource 69 // and Schema Guide whose Type is "string (URI)" and the name does not end in a 70 // case-insensitive form of "uri". That version of the schema is associated 71 // with version 1.16.0 of the Redfish Specification. Going forward, new URI 72 // properties should end in URI so this list should not need to be maintained as 73 // the spec is updated. NOTE: These have been pre-sorted in order to be 74 // compatible with binary search 75 constexpr std::array nonUriProperties{ 76 "@Redfish.ActionInfo", 77 // "@odata.context", // We can't fix /redfish/v1/$metadata URIs 78 "@odata.id", 79 // "Destination", // Only used by EventService and won't be a Redfish URI 80 // "HostName", // Isn't actually a Redfish URI 81 "Image", 82 "MetricProperty", 83 // "OriginOfCondition", // Is URI when in request, but is object in response 84 "TaskMonitor", 85 "target", // normal string, but target URI for POST to invoke an action 86 }; 87 // clang-format on 88 89 // Search the top collection array to determine if the passed URI is of a 90 // desired type 91 inline bool searchCollectionsArray(std::string_view uri, 92 const SearchType searchType) 93 { 94 boost::system::result<boost::urls::url> parsedUrl = 95 boost::urls::parse_relative_ref(uri); 96 if (!parsedUrl) 97 { 98 BMCWEB_LOG_ERROR("Failed to get target URI from {}", uri); 99 return false; 100 } 101 102 parsedUrl->normalize(); 103 boost::urls::segments_ref segments = parsedUrl->segments(); 104 if (!segments.is_absolute()) 105 { 106 return false; 107 } 108 109 // The passed URI must begin with "/redfish/v1", but we have to strip it 110 // from the URI since topCollections does not include it in its URIs. 111 if (segments.size() < 2) 112 { 113 return false; 114 } 115 if (segments.front() != "redfish") 116 { 117 return false; 118 } 119 segments.erase(segments.begin()); 120 if (segments.front() != "v1") 121 { 122 return false; 123 } 124 segments.erase(segments.begin()); 125 126 // Exclude the trailing "/" if it exists such as in "/redfish/v1/". 127 if (!segments.empty() && segments.back().empty()) 128 { 129 segments.pop_back(); 130 } 131 132 // If no segments then the passed URI was either "/redfish/v1" or 133 // "/redfish/v1/". 134 if (segments.empty()) 135 { 136 return (searchType == SearchType::ContainsSubordinate) || 137 (searchType == SearchType::CollOrCon); 138 } 139 std::string_view url = segments.buffer(); 140 const auto* it = std::ranges::lower_bound(topCollections, url); 141 if (it == topCollections.end()) 142 { 143 // parsedUrl is alphabetically after the last entry in the array so it 144 // can't be a top collection or up tree from a top collection 145 return false; 146 } 147 148 boost::urls::url collectionUrl(*it); 149 boost::urls::segments_view collectionSegments = collectionUrl.segments(); 150 boost::urls::segments_view::iterator itCollection = 151 collectionSegments.begin(); 152 const boost::urls::segments_view::const_iterator endCollection = 153 collectionSegments.end(); 154 155 // Each segment in the passed URI should match the found collection 156 for (const auto& segment : segments) 157 { 158 if (itCollection == endCollection) 159 { 160 // Leftover segments means the target is for an aggregation 161 // supported resource 162 return searchType == SearchType::Resource; 163 } 164 165 if (segment != (*itCollection)) 166 { 167 return false; 168 } 169 itCollection++; 170 } 171 172 // No remaining segments means the passed URI was a top level collection 173 if (searchType == SearchType::Collection) 174 { 175 return itCollection == endCollection; 176 } 177 if (searchType == SearchType::ContainsSubordinate) 178 { 179 return itCollection != endCollection; 180 } 181 182 // Return this check instead of "true" in case other SearchTypes get added 183 return searchType == SearchType::CollOrCon; 184 } 185 186 // Determines if the passed property contains a URI. Those property names 187 // either end with a case-insensitive version of "uri" or are specifically 188 // defined in the above array. 189 inline bool isPropertyUri(std::string_view propertyName) 190 { 191 if (propertyName.ends_with("uri") || propertyName.ends_with("Uri") || 192 propertyName.ends_with("URI")) 193 { 194 return true; 195 } 196 return std::binary_search(nonUriProperties.begin(), nonUriProperties.end(), 197 propertyName); 198 } 199 200 inline void addPrefixToStringItem(std::string& strValue, 201 std::string_view prefix) 202 { 203 // Make sure the value is a properly formatted URI 204 auto parsed = boost::urls::parse_relative_ref(strValue); 205 if (!parsed) 206 { 207 // Note that DMTF URIs such as 208 // https://redfish.dmtf.org/registries/Base.1.15.0.json will fail this 209 // check and that's okay 210 BMCWEB_LOG_DEBUG("Couldn't parse URI from resource {}", strValue); 211 return; 212 } 213 214 const boost::urls::url_view& thisUrl = *parsed; 215 216 // We don't need to aggregate JsonSchemas due to potential issues such as 217 // version mismatches between aggregator and satellite BMCs. For now 218 // assume that the aggregator has all the schemas and versions that the 219 // aggregated server has. 220 if (crow::utility::readUrlSegments(thisUrl, "redfish", "v1", "JsonSchemas", 221 crow::utility::OrMorePaths())) 222 { 223 BMCWEB_LOG_DEBUG("Skipping JsonSchemas URI prefix fixing"); 224 return; 225 } 226 227 // The first two segments should be "/redfish/v1". We need to check that 228 // before we can search topCollections 229 if (!crow::utility::readUrlSegments(thisUrl, "redfish", "v1", 230 crow::utility::OrMorePaths())) 231 { 232 return; 233 } 234 235 // Check array adding a segment each time until collection is identified 236 // Add prefix to segment after the collection 237 const boost::urls::segments_view urlSegments = thisUrl.segments(); 238 bool addedPrefix = false; 239 boost::urls::url url("/"); 240 boost::urls::segments_view::const_iterator it = urlSegments.begin(); 241 const boost::urls::segments_view::const_iterator end = urlSegments.end(); 242 243 // Skip past the leading "/redfish/v1" 244 it++; 245 it++; 246 for (; it != end; it++) 247 { 248 // Trailing "/" will result in an empty segment. In that case we need 249 // to return so we don't apply a prefix to top level collections such 250 // as "/redfish/v1/Chassis/" 251 if ((*it).empty()) 252 { 253 return; 254 } 255 256 if (std::binary_search(topCollections.begin(), topCollections.end(), 257 url.buffer())) 258 { 259 std::string collectionItem(prefix); 260 collectionItem += "_" + (*it); 261 url.segments().push_back(collectionItem); 262 it++; 263 addedPrefix = true; 264 break; 265 } 266 267 url.segments().push_back(*it); 268 } 269 270 // Finish constructing the URL here (if needed) to avoid additional checks 271 for (; it != end; it++) 272 { 273 url.segments().push_back(*it); 274 } 275 276 if (addedPrefix) 277 { 278 url.segments().insert(url.segments().begin(), {"redfish", "v1"}); 279 strValue = url.buffer(); 280 } 281 } 282 283 inline void addPrefixToItem(nlohmann::json& item, std::string_view prefix) 284 { 285 std::string* strValue = item.get_ptr<std::string*>(); 286 if (strValue == nullptr) 287 { 288 // Values for properties like "InvalidURI" and "ResourceMissingAtURI" 289 // from within the Base Registry are objects instead of strings and will 290 // fall into this check 291 BMCWEB_LOG_DEBUG("Field was not a string"); 292 return; 293 } 294 addPrefixToStringItem(*strValue, prefix); 295 item = *strValue; 296 } 297 298 inline void addAggregatedHeaders(crow::Response& asyncResp, 299 const crow::Response& resp, 300 std::string_view prefix) 301 { 302 if (!resp.getHeaderValue("Content-Type").empty()) 303 { 304 asyncResp.addHeader(boost::beast::http::field::content_type, 305 resp.getHeaderValue("Content-Type")); 306 } 307 if (!resp.getHeaderValue("Allow").empty()) 308 { 309 asyncResp.addHeader(boost::beast::http::field::allow, 310 resp.getHeaderValue("Allow")); 311 } 312 std::string_view header = resp.getHeaderValue("Location"); 313 if (!header.empty()) 314 { 315 std::string location(header); 316 addPrefixToStringItem(location, prefix); 317 asyncResp.addHeader(boost::beast::http::field::location, location); 318 } 319 if (!resp.getHeaderValue("Retry-After").empty()) 320 { 321 asyncResp.addHeader(boost::beast::http::field::retry_after, 322 resp.getHeaderValue("Retry-After")); 323 } 324 // TODO: we need special handling for Link Header Value 325 } 326 327 // Fix HTTP headers which appear in responses from Task resources among others 328 inline void addPrefixToHeadersInResp(nlohmann::json& json, 329 std::string_view prefix) 330 { 331 // The passed in "HttpHeaders" should be an array of headers 332 nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>(); 333 if (array == nullptr) 334 { 335 BMCWEB_LOG_ERROR("Field wasn't an array_t????"); 336 return; 337 } 338 339 for (nlohmann::json& item : *array) 340 { 341 // Each header is a single string with the form "<Field>: <Value>" 342 std::string* strHeader = item.get_ptr<std::string*>(); 343 if (strHeader == nullptr) 344 { 345 BMCWEB_LOG_CRITICAL("Field wasn't a string????"); 346 continue; 347 } 348 349 constexpr std::string_view location = "Location: "; 350 if (strHeader->starts_with(location)) 351 { 352 std::string header = strHeader->substr(location.size()); 353 addPrefixToStringItem(header, prefix); 354 *strHeader = std::string(location) + header; 355 } 356 } 357 } 358 359 // Search the json for all URIs and add the supplied prefix if the URI is for 360 // an aggregated resource. 361 inline void addPrefixes(nlohmann::json& json, std::string_view prefix) 362 { 363 nlohmann::json::object_t* object = 364 json.get_ptr<nlohmann::json::object_t*>(); 365 if (object != nullptr) 366 { 367 for (std::pair<const std::string, nlohmann::json>& item : *object) 368 { 369 if (isPropertyUri(item.first)) 370 { 371 addPrefixToItem(item.second, prefix); 372 continue; 373 } 374 375 // "HttpHeaders" contains HTTP headers. Among those we need to 376 // attempt to fix the "Location" header 377 if (item.first == "HttpHeaders") 378 { 379 addPrefixToHeadersInResp(item.second, prefix); 380 continue; 381 } 382 383 // Recursively parse the rest of the json 384 addPrefixes(item.second, prefix); 385 } 386 return; 387 } 388 nlohmann::json::array_t* array = json.get_ptr<nlohmann::json::array_t*>(); 389 if (array != nullptr) 390 { 391 for (nlohmann::json& item : *array) 392 { 393 addPrefixes(item, prefix); 394 } 395 } 396 } 397 398 inline boost::system::error_code aggregationRetryHandler(unsigned int respCode) 399 { 400 // Allow all response codes because we want to surface any satellite 401 // issue to the client 402 BMCWEB_LOG_DEBUG("Received {} response from satellite", respCode); 403 return boost::system::errc::make_error_code(boost::system::errc::success); 404 } 405 406 inline crow::ConnectionPolicy getAggregationPolicy() 407 { 408 return {.maxRetryAttempts = 0, 409 .requestByteLimit = aggregatorReadBodyLimit, 410 .maxConnections = 20, 411 .retryPolicyAction = "TerminateAfterRetries", 412 .retryIntervalSecs = std::chrono::seconds(0), 413 .invalidResp = aggregationRetryHandler}; 414 } 415 416 struct AggregationSource 417 { 418 boost::urls::url url; 419 std::string username; 420 std::string password; 421 }; 422 423 class RedfishAggregator 424 { 425 private: 426 crow::HttpClient client; 427 428 // Dummy callback used by the Constructor so that it can report the number 429 // of satellite configs when the class is first created 430 static void constructorCallback( 431 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 432 { 433 BMCWEB_LOG_DEBUG("There were {} satellite configs found at startup", 434 std::to_string(satelliteInfo.size())); 435 } 436 437 // Search D-Bus objects for satellite config objects and add their 438 // information if valid 439 static void findSatelliteConfigs( 440 const dbus::utility::ManagedObjectType& objects, 441 std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 442 { 443 for (const auto& objectPath : objects) 444 { 445 for (const auto& interface : objectPath.second) 446 { 447 if (interface.first == 448 "xyz.openbmc_project.Configuration.SatelliteController") 449 { 450 BMCWEB_LOG_DEBUG("Found Satellite Controller at {}", 451 objectPath.first.str); 452 453 if (!satelliteInfo.empty()) 454 { 455 BMCWEB_LOG_ERROR( 456 "Redfish Aggregation only supports one satellite!"); 457 BMCWEB_LOG_DEBUG("Clearing all satellite data"); 458 satelliteInfo.clear(); 459 return; 460 } 461 462 addSatelliteConfig(interface.second, satelliteInfo); 463 } 464 } 465 } 466 } 467 468 // Parse the properties of a satellite config object and add the 469 // configuration if the properties are valid 470 static void addSatelliteConfig( 471 const dbus::utility::DBusPropertiesMap& properties, 472 std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 473 { 474 boost::urls::url url; 475 std::string prefix; 476 477 for (const auto& prop : properties) 478 { 479 if (prop.first == "Hostname") 480 { 481 const std::string* propVal = 482 std::get_if<std::string>(&prop.second); 483 if (propVal == nullptr) 484 { 485 BMCWEB_LOG_ERROR("Invalid Hostname value"); 486 return; 487 } 488 url.set_host(*propVal); 489 } 490 491 else if (prop.first == "Port") 492 { 493 const uint64_t* propVal = std::get_if<uint64_t>(&prop.second); 494 if (propVal == nullptr) 495 { 496 BMCWEB_LOG_ERROR("Invalid Port value"); 497 return; 498 } 499 500 if (*propVal > std::numeric_limits<uint16_t>::max()) 501 { 502 BMCWEB_LOG_ERROR("Port value out of range"); 503 return; 504 } 505 url.set_port(std::to_string(static_cast<uint16_t>(*propVal))); 506 } 507 508 else if (prop.first == "AuthType") 509 { 510 const std::string* propVal = 511 std::get_if<std::string>(&prop.second); 512 if (propVal == nullptr) 513 { 514 BMCWEB_LOG_ERROR("Invalid AuthType value"); 515 return; 516 } 517 518 // For now assume authentication not required to communicate 519 // with the satellite BMC 520 if (*propVal != "None") 521 { 522 BMCWEB_LOG_ERROR( 523 "Unsupported AuthType value: {}, only \"none\" is supported", 524 *propVal); 525 return; 526 } 527 url.set_scheme("http"); 528 } 529 else if (prop.first == "Name") 530 { 531 const std::string* propVal = 532 std::get_if<std::string>(&prop.second); 533 if (propVal != nullptr && !propVal->empty()) 534 { 535 prefix = *propVal; 536 BMCWEB_LOG_DEBUG("Using Name property {} as prefix", 537 prefix); 538 } 539 else 540 { 541 BMCWEB_LOG_DEBUG( 542 "Invalid or empty Name property, invalid satellite config"); 543 return; 544 } 545 } 546 } // Finished reading properties 547 548 // Make sure all required config information was made available 549 if (url.host().empty()) 550 { 551 BMCWEB_LOG_ERROR("Satellite config {} missing Host", prefix); 552 return; 553 } 554 555 if (!url.has_port()) 556 { 557 BMCWEB_LOG_ERROR("Satellite config {} missing Port", prefix); 558 return; 559 } 560 561 if (!url.has_scheme()) 562 { 563 BMCWEB_LOG_ERROR("Satellite config {} missing AuthType", prefix); 564 return; 565 } 566 567 std::string resultString; 568 auto result = satelliteInfo.insert_or_assign(prefix, std::move(url)); 569 if (result.second) 570 { 571 resultString = "Added new satellite config "; 572 } 573 else 574 { 575 resultString = "Updated existing satellite config "; 576 } 577 578 BMCWEB_LOG_DEBUG("{}{} at {}://{}", resultString, prefix, 579 result.first->second.scheme(), 580 result.first->second.encoded_host_and_port()); 581 } 582 583 enum AggregationType 584 { 585 Collection, 586 ContainsSubordinate, 587 Resource, 588 }; 589 590 void startAggregation( 591 AggregationType aggType, const crow::Request& thisReq, 592 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) const 593 { 594 if (thisReq.method() != boost::beast::http::verb::get) 595 { 596 if (aggType == AggregationType::Collection) 597 { 598 BMCWEB_LOG_DEBUG( 599 "Only aggregate GET requests to top level collections"); 600 return; 601 } 602 603 if (aggType == AggregationType::ContainsSubordinate) 604 { 605 BMCWEB_LOG_DEBUG( 606 "Only aggregate GET requests when uptree of a top level collection"); 607 return; 608 } 609 } 610 611 // Create a copy of thisReq so we we can still locally process the req 612 std::error_code ec; 613 auto localReq = std::make_shared<crow::Request>(thisReq.copy()); 614 if (ec) 615 { 616 BMCWEB_LOG_ERROR("Failed to create copy of request"); 617 if (aggType == AggregationType::Resource) 618 { 619 messages::internalError(asyncResp->res); 620 } 621 return; 622 } 623 624 if (aggType == AggregationType::Collection) 625 { 626 boost::urls::url& urlNew = localReq->url(); 627 auto paramsIt = urlNew.params().begin(); 628 while (paramsIt != urlNew.params().end()) 629 { 630 const boost::urls::param& param = *paramsIt; 631 // only and $skip, params can't be passed to satellite 632 // as applying these filters twice results in different results. 633 // Removing them will cause them to only be processed in the 634 // aggregator. Note, this still doesn't work for collections 635 // that might return less than the complete collection by 636 // default, but hopefully those are rare/nonexistent in top 637 // collections. bmcweb doesn't implement any of these. 638 if (param.key == "only" || param.key == "$skip") 639 { 640 BMCWEB_LOG_DEBUG( 641 "Erasing \"{}\" param from request to top level collection", 642 param.key); 643 644 paramsIt = urlNew.params().erase(paramsIt); 645 continue; 646 } 647 // Pass all other parameters 648 paramsIt++; 649 } 650 localReq->target(urlNew.buffer()); 651 } 652 653 getSatelliteConfigs( 654 std::bind_front(aggregateAndHandle, aggType, localReq, asyncResp)); 655 } 656 657 static void findSatellite( 658 const crow::Request& req, 659 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 660 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo, 661 std::string_view memberName) 662 { 663 // Determine if the resource ID begins with a known prefix 664 for (const auto& satellite : satelliteInfo) 665 { 666 std::string targetPrefix = satellite.first; 667 targetPrefix += "_"; 668 if (memberName.starts_with(targetPrefix)) 669 { 670 BMCWEB_LOG_DEBUG("\"{}\" is a known prefix", satellite.first); 671 672 // Remove the known prefix from the request's URI and 673 // then forward to the associated satellite BMC 674 getInstance().forwardRequest(req, asyncResp, satellite.first, 675 satelliteInfo); 676 return; 677 } 678 } 679 680 // We didn't recognize the prefix and need to return a 404 681 std::string nameStr = req.url().segments().back(); 682 messages::resourceNotFound(asyncResp->res, "", nameStr); 683 } 684 685 // Intended to handle an incoming request based on if Redfish Aggregation 686 // is enabled. Forwards request to satellite BMC if it exists. 687 static void aggregateAndHandle( 688 AggregationType aggType, 689 const std::shared_ptr<crow::Request>& sharedReq, 690 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 691 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 692 { 693 if (sharedReq == nullptr) 694 { 695 return; 696 } 697 698 // No satellite configs means we don't need to keep attempting to 699 // aggregate 700 if (satelliteInfo.empty()) 701 { 702 // For collections or resources that can contain a subordinate 703 // top level collection we'll also handle the request locally so we 704 // don't need to write an error code 705 if (aggType == AggregationType::Resource) 706 { 707 std::string nameStr = sharedReq->url().segments().back(); 708 messages::resourceNotFound(asyncResp->res, "", nameStr); 709 } 710 return; 711 } 712 713 const crow::Request& thisReq = *sharedReq; 714 BMCWEB_LOG_DEBUG("Aggregation is enabled, begin processing of {}", 715 thisReq.target()); 716 717 // We previously determined the request is for a collection. No need to 718 // check again 719 if (aggType == AggregationType::Collection) 720 { 721 BMCWEB_LOG_DEBUG("Aggregating a collection"); 722 // We need to use a specific response handler and send the 723 // request to all known satellites 724 getInstance().forwardCollectionRequests(thisReq, asyncResp, 725 satelliteInfo); 726 return; 727 } 728 729 // We previously determined the request may contain a subordinate 730 // collection. No need to check again 731 if (aggType == AggregationType::ContainsSubordinate) 732 { 733 BMCWEB_LOG_DEBUG( 734 "Aggregating what may have a subordinate collection"); 735 // We need to use a specific response handler and send the 736 // request to all known satellites 737 getInstance().forwardContainsSubordinateRequests(thisReq, asyncResp, 738 satelliteInfo); 739 return; 740 } 741 742 const boost::urls::segments_view urlSegments = thisReq.url().segments(); 743 boost::urls::url currentUrl("/"); 744 boost::urls::segments_view::const_iterator it = urlSegments.begin(); 745 boost::urls::segments_view::const_iterator end = urlSegments.end(); 746 747 // Skip past the leading "/redfish/v1" 748 it++; 749 it++; 750 for (; it != end; it++) 751 { 752 if (std::binary_search(topCollections.begin(), topCollections.end(), 753 currentUrl.buffer())) 754 { 755 // We've matched a resource collection so this current segment 756 // must contain an aggregation prefix 757 findSatellite(thisReq, asyncResp, satelliteInfo, *it); 758 return; 759 } 760 761 currentUrl.segments().push_back(*it); 762 } 763 764 // We shouldn't reach this point since we should've hit one of the 765 // previous exits 766 messages::internalError(asyncResp->res); 767 } 768 769 // Attempt to forward a request to the satellite BMC associated with the 770 // prefix. 771 void forwardRequest( 772 const crow::Request& thisReq, 773 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 774 const std::string& prefix, 775 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 776 { 777 const auto& sat = satelliteInfo.find(prefix); 778 if (sat == satelliteInfo.end()) 779 { 780 // Realistically this shouldn't get called since we perform an 781 // earlier check to make sure the prefix exists 782 BMCWEB_LOG_ERROR("Unrecognized satellite prefix \"{}\"", prefix); 783 return; 784 } 785 786 // We need to strip the prefix from the request's path 787 boost::urls::url targetURI(thisReq.target()); 788 std::string path = thisReq.url().path(); 789 size_t pos = path.find(prefix + "_"); 790 if (pos == std::string::npos) 791 { 792 // If this fails then something went wrong 793 BMCWEB_LOG_ERROR("Error removing prefix \"{}_\" from request URI", 794 prefix); 795 messages::internalError(asyncResp->res); 796 return; 797 } 798 path.erase(pos, prefix.size() + 1); 799 800 std::function<void(crow::Response&)> cb = 801 std::bind_front(processResponse, prefix, asyncResp); 802 803 std::string data = thisReq.body(); 804 boost::urls::url url(sat->second); 805 url.set_path(path); 806 if (targetURI.has_query()) 807 { 808 url.set_query(targetURI.query()); 809 } 810 811 // Prepare request headers 812 boost::beast::http::fields requestFields = 813 prepareAggregationHeaders(thisReq.fields(), prefix); 814 815 client.sendDataWithCallback(std::move(data), url, 816 ensuressl::VerifyCertificate::Verify, 817 requestFields, thisReq.method(), cb); 818 } 819 820 // Forward a request for a collection URI to each known satellite BMC 821 void forwardCollectionRequests( 822 const crow::Request& thisReq, 823 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 824 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 825 { 826 for (const auto& sat : satelliteInfo) 827 { 828 std::function<void(crow::Response&)> cb = std::bind_front( 829 processCollectionResponse, sat.first, asyncResp); 830 831 boost::urls::url url(sat.second); 832 url.set_path(thisReq.url().path()); 833 if (thisReq.url().has_query()) 834 { 835 url.set_query(thisReq.url().query()); 836 } 837 std::string data = thisReq.body(); 838 839 // Prepare request headers 840 boost::beast::http::fields requestFields = 841 prepareAggregationHeaders(thisReq.fields(), sat.first); 842 843 client.sendDataWithCallback(std::move(data), url, 844 ensuressl::VerifyCertificate::Verify, 845 requestFields, thisReq.method(), cb); 846 } 847 } 848 849 // Forward request for a URI that is uptree of a top level collection to 850 // each known satellite BMC 851 void forwardContainsSubordinateRequests( 852 const crow::Request& thisReq, 853 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 854 const std::unordered_map<std::string, boost::urls::url>& satelliteInfo) 855 { 856 for (const auto& sat : satelliteInfo) 857 { 858 std::function<void(crow::Response&)> cb = std::bind_front( 859 processContainsSubordinateResponse, sat.first, asyncResp); 860 861 // will ignore an expanded resource in the response if that resource 862 // is not already supported by the aggregating BMC 863 // TODO: Improve the processing so that we don't have to strip query 864 // params in this specific case 865 boost::urls::url url(sat.second); 866 url.set_path(thisReq.url().path()); 867 868 std::string data = thisReq.body(); 869 870 // Prepare request headers 871 boost::beast::http::fields requestFields = 872 prepareAggregationHeaders(thisReq.fields(), sat.first); 873 874 client.sendDataWithCallback(std::move(data), url, 875 ensuressl::VerifyCertificate::Verify, 876 requestFields, thisReq.method(), cb); 877 } 878 } 879 880 public: 881 explicit RedfishAggregator() : 882 client(getIoContext(), 883 std::make_shared<crow::ConnectionPolicy>(getAggregationPolicy())) 884 { 885 getSatelliteConfigs(constructorCallback); 886 } 887 RedfishAggregator(const RedfishAggregator&) = delete; 888 RedfishAggregator& operator=(const RedfishAggregator&) = delete; 889 RedfishAggregator(RedfishAggregator&&) = delete; 890 RedfishAggregator& operator=(RedfishAggregator&&) = delete; 891 ~RedfishAggregator() = default; 892 893 static RedfishAggregator& getInstance() 894 { 895 static RedfishAggregator handler; 896 return handler; 897 } 898 899 // Aggregation sources with their URLs and optional credentials 900 std::unordered_map<std::string, AggregationSource> aggregationSources; 901 902 // Helper function to prepare headers for aggregated satellite BMC requests 903 boost::beast::http::fields prepareAggregationHeaders( 904 const boost::beast::http::fields& originalFields, 905 const std::string& prefix) const 906 { 907 boost::beast::http::fields fields = originalFields; 908 909 // POST AggregationService can only parse JSON 910 fields.set(boost::beast::http::field::accept, "application/json"); 911 912 // Add authentication if credentials exist for this prefix 913 auto it = aggregationSources.find(prefix); 914 if (it != aggregationSources.end()) 915 { 916 const auto& source = it->second; 917 // Only add auth header if both username and password are provided 918 if (!source.username.empty() && !source.password.empty()) 919 { 920 std::string authHeader = crow::utility::createBasicAuthHeader( 921 source.username, source.password); 922 fields.set(boost::beast::http::field::authorization, 923 authHeader); 924 } 925 } 926 return fields; 927 } 928 929 // Polls D-Bus to get all available satellite config information 930 // Expects a handler which interacts with the returned configs 931 void getSatelliteConfigs( 932 std::function< 933 void(const std::unordered_map<std::string, boost::urls::url>&)> 934 handler) const 935 { 936 BMCWEB_LOG_DEBUG("Gathering satellite configs"); 937 938 // Extract just the URLs from aggregationSources for the handler 939 std::unordered_map<std::string, boost::urls::url> satelliteInfo; 940 for (const auto& [prefix, source] : aggregationSources) 941 { 942 satelliteInfo.emplace(prefix, source.url); 943 } 944 945 sdbusplus::message::object_path path("/xyz/openbmc_project/inventory"); 946 dbus::utility::getManagedObjects( 947 "xyz.openbmc_project.EntityManager", path, 948 [handler{std::move(handler)}, 949 satelliteInfo = std::move(satelliteInfo)]( 950 const boost::system::error_code& ec, 951 const dbus::utility::ManagedObjectType& objects) mutable { 952 if (ec) 953 { 954 BMCWEB_LOG_WARNING("DBUS response error {}, {}", ec.value(), 955 ec.message()); 956 } 957 else 958 { 959 // Maps a chosen alias representing a satellite BMC to a url 960 // containing the information required to create a http 961 // connection to the satellite 962 findSatelliteConfigs(objects, satelliteInfo); 963 964 if (!satelliteInfo.empty()) 965 { 966 BMCWEB_LOG_DEBUG( 967 "Redfish Aggregation enabled with {} satellite BMCs", 968 std::to_string(satelliteInfo.size())); 969 } 970 else 971 { 972 BMCWEB_LOG_DEBUG( 973 "No satellite BMCs detected. Redfish Aggregation not enabled"); 974 } 975 } 976 handler(satelliteInfo); 977 }); 978 } 979 980 // Processes the response returned by a satellite BMC and loads its 981 // contents into asyncResp 982 static void processResponse( 983 std::string_view prefix, 984 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 985 crow::Response& resp) 986 { 987 // 429 and 502 mean we didn't actually send the request so don't 988 // overwrite the response headers in that case 989 if ((resp.result() == boost::beast::http::status::too_many_requests) || 990 (resp.result() == boost::beast::http::status::bad_gateway)) 991 { 992 asyncResp->res.result(resp.result()); 993 return; 994 } 995 996 // We want to attempt prefix fixing regardless of response code 997 // The resp will not have a json component 998 // We need to create a json from resp's stringResponse 999 if (isJsonContentType(resp.getHeaderValue("Content-Type"))) 1000 { 1001 nlohmann::json jsonVal = 1002 nlohmann::json::parse(*resp.body(), nullptr, false); 1003 if (jsonVal.is_discarded()) 1004 { 1005 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON"); 1006 messages::operationFailed(asyncResp->res); 1007 return; 1008 } 1009 1010 BMCWEB_LOG_DEBUG("Successfully parsed satellite response"); 1011 1012 addPrefixes(jsonVal, prefix); 1013 1014 BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response"); 1015 1016 asyncResp->res.result(resp.result()); 1017 asyncResp->res.jsonValue = std::move(jsonVal); 1018 1019 BMCWEB_LOG_DEBUG("Finished writing asyncResp"); 1020 } 1021 else 1022 { 1023 // We allow any Content-Type that is not "application/json" now 1024 asyncResp->res.result(resp.result()); 1025 asyncResp->res.copyBody(resp); 1026 } 1027 addAggregatedHeaders(asyncResp->res, resp, prefix); 1028 } 1029 1030 // Processes the collection response returned by a satellite BMC and merges 1031 // its "@odata.id" values 1032 static void processCollectionResponse( 1033 const std::string& prefix, 1034 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 1035 crow::Response& resp) 1036 { 1037 // 429 and 502 mean we didn't actually send the request so don't 1038 // overwrite the response headers in that case 1039 if ((resp.result() == boost::beast::http::status::too_many_requests) || 1040 (resp.result() == boost::beast::http::status::bad_gateway)) 1041 { 1042 return; 1043 } 1044 1045 if (resp.resultInt() != 200) 1046 { 1047 BMCWEB_LOG_DEBUG( 1048 "Collection resource does not exist in satellite BMC \"{}\"", 1049 prefix); 1050 // Return the error if we haven't had any successes 1051 if (asyncResp->res.resultInt() != 200) 1052 { 1053 asyncResp->res.result(resp.result()); 1054 asyncResp->res.copyBody(resp); 1055 } 1056 return; 1057 } 1058 1059 // The resp will not have a json component 1060 // We need to create a json from resp's stringResponse 1061 if (isJsonContentType(resp.getHeaderValue("Content-Type"))) 1062 { 1063 nlohmann::json jsonVal = 1064 nlohmann::json::parse(*resp.body(), nullptr, false); 1065 if (jsonVal.is_discarded()) 1066 { 1067 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON"); 1068 1069 // Notify the user if doing so won't overwrite a valid response 1070 if (asyncResp->res.resultInt() != 200) 1071 { 1072 messages::operationFailed(asyncResp->res); 1073 } 1074 return; 1075 } 1076 1077 BMCWEB_LOG_DEBUG("Successfully parsed satellite response"); 1078 1079 // Now we need to add the prefix to the URIs contained in the 1080 // response. 1081 addPrefixes(jsonVal, prefix); 1082 1083 BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response"); 1084 1085 // If this resource collection does not exist on the aggregating bmc 1086 // and has not already been added from processing the response from 1087 // a different satellite then we need to completely overwrite 1088 // asyncResp 1089 if (asyncResp->res.resultInt() != 200) 1090 { 1091 // We only want to aggregate collections that contain a 1092 // "Members" array 1093 if ((!jsonVal.contains("Members")) && 1094 (!jsonVal["Members"].is_array())) 1095 { 1096 BMCWEB_LOG_DEBUG( 1097 "Skipping aggregating unsupported resource"); 1098 return; 1099 } 1100 1101 BMCWEB_LOG_DEBUG( 1102 "Collection does not exist, overwriting asyncResp"); 1103 asyncResp->res.result(resp.result()); 1104 asyncResp->res.jsonValue = std::move(jsonVal); 1105 asyncResp->res.addHeader("Content-Type", "application/json"); 1106 1107 BMCWEB_LOG_DEBUG("Finished overwriting asyncResp"); 1108 } 1109 else 1110 { 1111 // We only want to aggregate collections that contain a 1112 // "Members" array 1113 if ((!asyncResp->res.jsonValue.contains("Members")) && 1114 (!asyncResp->res.jsonValue["Members"].is_array())) 1115 1116 { 1117 BMCWEB_LOG_DEBUG( 1118 "Skipping aggregating unsupported resource"); 1119 return; 1120 } 1121 1122 BMCWEB_LOG_DEBUG( 1123 "Adding aggregated resources from \"{}\" to collection", 1124 prefix); 1125 1126 // TODO: This is a potential race condition with multiple 1127 // satellites and the aggregating bmc attempting to write to 1128 // update this array. May need to cascade calls to the next 1129 // satellite at the end of this function. 1130 // This is presumably not a concern when there is only a single 1131 // satellite since the aggregating bmc should have completed 1132 // before the response is received from the satellite. 1133 1134 auto& members = asyncResp->res.jsonValue["Members"]; 1135 auto& satMembers = jsonVal["Members"]; 1136 for (auto& satMem : satMembers) 1137 { 1138 members.emplace_back(std::move(satMem)); 1139 } 1140 asyncResp->res.jsonValue["Members@odata.count"] = 1141 members.size(); 1142 1143 // TODO: Do we need to sort() after updating the array? 1144 } 1145 } 1146 else 1147 { 1148 BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"", 1149 prefix); 1150 // We received a response that was not a json. 1151 // Notify the user only if we did not receive any valid responses 1152 // and if the resource collection does not already exist on the 1153 // aggregating BMC 1154 if (asyncResp->res.resultInt() != 200) 1155 { 1156 messages::operationFailed(asyncResp->res); 1157 } 1158 } 1159 } // End processCollectionResponse() 1160 1161 // Processes the response returned by a satellite BMC and merges any 1162 // properties whose "@odata.id" value is the URI or either a top level 1163 // collection or is uptree from a top level collection 1164 static void processContainsSubordinateResponse( 1165 const std::string& prefix, 1166 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp, 1167 crow::Response& resp) 1168 { 1169 // 429 and 502 mean we didn't actually send the request so don't 1170 // overwrite the response headers in that case 1171 if ((resp.result() == boost::beast::http::status::too_many_requests) || 1172 (resp.result() == boost::beast::http::status::bad_gateway)) 1173 { 1174 return; 1175 } 1176 1177 if (resp.resultInt() != 200) 1178 { 1179 BMCWEB_LOG_DEBUG( 1180 "Resource uptree from Collection does not exist in satellite BMC \"{}\"", 1181 prefix); 1182 // Return the error if we haven't had any successes 1183 if (asyncResp->res.resultInt() != 200) 1184 { 1185 asyncResp->res.result(resp.result()); 1186 asyncResp->res.copyBody(resp); 1187 } 1188 return; 1189 } 1190 1191 // The resp will not have a json component 1192 // We need to create a json from resp's stringResponse 1193 if (isJsonContentType(resp.getHeaderValue("Content-Type"))) 1194 { 1195 bool addedLinks = false; 1196 nlohmann::json jsonVal = 1197 nlohmann::json::parse(*resp.body(), nullptr, false); 1198 if (jsonVal.is_discarded()) 1199 { 1200 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON"); 1201 1202 // Notify the user if doing so won't overwrite a valid response 1203 if (asyncResp->res.resultInt() != 200) 1204 { 1205 messages::operationFailed(asyncResp->res); 1206 } 1207 return; 1208 } 1209 1210 BMCWEB_LOG_DEBUG("Successfully parsed satellite response"); 1211 1212 // Parse response and add properties missing from the AsyncResp 1213 // Valid properties will be of the form <property>.@odata.id and 1214 // @odata.id is a <URI>. In other words, the json should contain 1215 // multiple properties such that 1216 // {"<property>":{"@odata.id": "<URI>"}} 1217 nlohmann::json::object_t* object = 1218 jsonVal.get_ptr<nlohmann::json::object_t*>(); 1219 if (object == nullptr) 1220 { 1221 BMCWEB_LOG_ERROR("Parsed JSON was not an object?"); 1222 return; 1223 } 1224 1225 for (std::pair<const std::string, nlohmann::json>& prop : *object) 1226 { 1227 if (!prop.second.contains("@odata.id")) 1228 { 1229 continue; 1230 } 1231 1232 std::string* strValue = 1233 prop.second["@odata.id"].get_ptr<std::string*>(); 1234 if (strValue == nullptr) 1235 { 1236 BMCWEB_LOG_CRITICAL("Field wasn't a string????"); 1237 continue; 1238 } 1239 if (!searchCollectionsArray(*strValue, SearchType::CollOrCon)) 1240 { 1241 continue; 1242 } 1243 1244 addedLinks = true; 1245 if (!asyncResp->res.jsonValue.contains(prop.first)) 1246 { 1247 // Only add the property if it did not already exist 1248 BMCWEB_LOG_DEBUG("Adding link for {} from BMC {}", 1249 *strValue, prefix); 1250 asyncResp->res.jsonValue[prop.first]["@odata.id"] = 1251 *strValue; 1252 continue; 1253 } 1254 } 1255 1256 // If we added links to a previously unsuccessful (non-200) response 1257 // then we need to make sure the response contains the bare minimum 1258 // amount of additional information that we'd expect to have been 1259 // populated. 1260 if (addedLinks && (asyncResp->res.resultInt() != 200)) 1261 { 1262 // This resource didn't locally exist or an error 1263 // occurred while generating the response. Remove any 1264 // error messages and update the error code. 1265 asyncResp->res.jsonValue.erase( 1266 asyncResp->res.jsonValue.find("error")); 1267 asyncResp->res.result(resp.result()); 1268 1269 const auto& it1 = object->find("@odata.id"); 1270 if (it1 != object->end()) 1271 { 1272 asyncResp->res.jsonValue["@odata.id"] = (it1->second); 1273 } 1274 const auto& it2 = object->find("@odata.type"); 1275 if (it2 != object->end()) 1276 { 1277 asyncResp->res.jsonValue["@odata.type"] = (it2->second); 1278 } 1279 const auto& it3 = object->find("Id"); 1280 if (it3 != object->end()) 1281 { 1282 asyncResp->res.jsonValue["Id"] = (it3->second); 1283 } 1284 const auto& it4 = object->find("Name"); 1285 if (it4 != object->end()) 1286 { 1287 asyncResp->res.jsonValue["Name"] = (it4->second); 1288 } 1289 } 1290 } 1291 else 1292 { 1293 BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"", 1294 prefix); 1295 // We received as response that was not a json 1296 // Notify the user only if we did not receive any valid responses, 1297 // and if the resource does not already exist on the aggregating BMC 1298 if (asyncResp->res.resultInt() != 200) 1299 { 1300 messages::operationFailed(asyncResp->res); 1301 } 1302 } 1303 } 1304 1305 // Entry point to Redfish Aggregation 1306 // Returns Result stating whether or not we still need to locally handle the 1307 // request 1308 Result beginAggregation(const crow::Request& thisReq, 1309 const std::shared_ptr<bmcweb::AsyncResp>& asyncResp) 1310 { 1311 using crow::utility::OrMorePaths; 1312 using crow::utility::readUrlSegments; 1313 boost::urls::url_view url = thisReq.url(); 1314 1315 // We don't need to aggregate JsonSchemas due to potential issues such 1316 // as version mismatches between aggregator and satellite BMCs. For 1317 // now assume that the aggregator has all the schemas and versions that 1318 // the aggregated server has. 1319 if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas", 1320 crow::utility::OrMorePaths())) 1321 { 1322 return Result::LocalHandle; 1323 } 1324 1325 // The first two segments should be "/redfish/v1". We need to check 1326 // that before we can search topCollections 1327 if (!crow::utility::readUrlSegments(url, "redfish", "v1", 1328 crow::utility::OrMorePaths())) 1329 { 1330 return Result::LocalHandle; 1331 } 1332 1333 // Parse the URI to see if it begins with a known top level collection 1334 // such as: 1335 // /redfish/v1/Chassis 1336 // /redfish/v1/UpdateService/FirmwareInventory 1337 const boost::urls::segments_view urlSegments = url.segments(); 1338 boost::urls::url currentUrl("/"); 1339 boost::urls::segments_view::const_iterator it = urlSegments.begin(); 1340 boost::urls::segments_view::const_iterator end = urlSegments.end(); 1341 1342 // Skip past the leading "/redfish/v1" 1343 it++; 1344 it++; 1345 for (; it != end; it++) 1346 { 1347 const std::string& collectionItem = *it; 1348 if (std::binary_search(topCollections.begin(), topCollections.end(), 1349 currentUrl.buffer())) 1350 { 1351 // We've matched a resource collection so this current segment 1352 // might contain an aggregation prefix 1353 if (segmentHasPrefix(collectionItem)) 1354 { 1355 BMCWEB_LOG_DEBUG("Need to forward a request"); 1356 1357 // Extract the prefix from the request's URI, retrieve the 1358 // associated satellite config information, and then forward 1359 // the request to that satellite. 1360 startAggregation(AggregationType::Resource, thisReq, 1361 asyncResp); 1362 return Result::NoLocalHandle; 1363 } 1364 1365 // Handle collection URI with a trailing backslash 1366 // e.g. /redfish/v1/Chassis/ 1367 it++; 1368 if ((it == end) && collectionItem.empty()) 1369 { 1370 startAggregation(AggregationType::Collection, thisReq, 1371 asyncResp); 1372 } 1373 1374 // We didn't recognize the prefix or it's a collection with a 1375 // trailing "/". In both cases we still want to locally handle 1376 // the request 1377 return Result::LocalHandle; 1378 } 1379 1380 currentUrl.segments().push_back(collectionItem); 1381 } 1382 1383 // If we made it here then currentUrl could contain a top level 1384 // collection URI without a trailing "/", e.g. /redfish/v1/Chassis 1385 if (std::binary_search(topCollections.begin(), topCollections.end(), 1386 currentUrl.buffer())) 1387 { 1388 startAggregation(AggregationType::Collection, thisReq, asyncResp); 1389 return Result::LocalHandle; 1390 } 1391 1392 // If nothing else then the request could be for a resource which has a 1393 // top level collection as a subordinate 1394 if (searchCollectionsArray(url.path(), SearchType::ContainsSubordinate)) 1395 { 1396 startAggregation(AggregationType::ContainsSubordinate, thisReq, 1397 asyncResp); 1398 return Result::LocalHandle; 1399 } 1400 1401 BMCWEB_LOG_DEBUG("Aggregation not required for {}", url.buffer()); 1402 return Result::LocalHandle; 1403 } 1404 1405 // Check if the given URL segment matches with any satellite prefix 1406 // Assumes the given segment starts with <prefix>_ 1407 bool segmentHasPrefix(const std::string& urlSegment) const 1408 { 1409 // TODO: handle this better 1410 // For now 5B247A_ wont be in the aggregationSources map so 1411 // check explicitly for now 1412 if (urlSegment.starts_with("5B247A_")) 1413 { 1414 return true; 1415 } 1416 1417 // Find the first underscore 1418 std::size_t underscorePos = urlSegment.find('_'); 1419 if (underscorePos == std::string::npos) 1420 { 1421 return false; // No underscore, can't be a satellite prefix 1422 } 1423 1424 // Extract the prefix 1425 std::string prefix = urlSegment.substr(0, underscorePos); 1426 1427 // Check if this prefix exists 1428 return aggregationSources.contains(prefix); 1429 } 1430 }; 1431 1432 } // namespace redfish 1433