xref: /openbmc/bmcweb/redfish-core/include/redfish_aggregator.hpp (revision 96a194c4a8cbd2c10dd44650031c064eddf30252)
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 #include "utils/redfish_aggregator_utils.hpp"
18 
19 #include <boost/beast/http/field.hpp>
20 #include <boost/beast/http/status.hpp>
21 #include <boost/beast/http/verb.hpp>
22 #include <boost/system/errc.hpp>
23 #include <boost/system/result.hpp>
24 #include <boost/url/param.hpp>
25 #include <boost/url/parse.hpp>
26 #include <boost/url/segments_ref.hpp>
27 #include <boost/url/segments_view.hpp>
28 #include <boost/url/url.hpp>
29 #include <boost/url/url_view.hpp>
30 #include <nlohmann/json.hpp>
31 #include <sdbusplus/message/native_types.hpp>
32 
33 #include <algorithm>
34 #include <array>
35 #include <chrono>
36 #include <cstddef>
37 #include <cstdint>
38 #include <functional>
39 #include <limits>
40 #include <memory>
41 #include <ranges>
42 #include <string>
43 #include <string_view>
44 #include <system_error>
45 #include <unordered_map>
46 #include <utility>
47 #include <variant>
48 
49 namespace redfish
50 {
51 
52 constexpr unsigned int aggregatorReadBodyLimit = 50 * 1024 * 1024; // 50MB
53 
54 enum class Result
55 {
56     LocalHandle,
57     NoLocalHandle
58 };
59 
60 enum class SearchType
61 {
62     Collection,
63     CollOrCon,
64     ContainsSubordinate,
65     Resource
66 };
67 
68 // clang-format off
69 // These are all of the properties as of version 2022.2 of the Redfish Resource
70 // and Schema Guide whose Type is "string (URI)" and the name does not end in a
71 // case-insensitive form of "uri".  That version of the schema is associated
72 // with version 1.16.0 of the Redfish Specification.  Going forward, new URI
73 // properties should end in URI so this list should not need to be maintained as
74 // the spec is updated.  NOTE: These have been pre-sorted in order to be
75 // compatible with binary search
76 constexpr std::array nonUriProperties{
77     "@Redfish.ActionInfo",
78     // "@odata.context", // We can't fix /redfish/v1/$metadata URIs
79     "@odata.id",
80     // "Destination", // Only used by EventService and won't be a Redfish URI
81     // "HostName", // Isn't actually a Redfish URI
82     "Image",
83     "MetricProperty",
84     // "OriginOfCondition", // Is URI when in request, but is object in response
85     "TaskMonitor",
86     "target", // normal string, but target URI for POST to invoke an action
87 };
88 // clang-format on
89 
90 // Search the top collection array to determine if the passed URI is of a
91 // desired type
searchCollectionsArray(std::string_view uri,const SearchType searchType)92 inline bool searchCollectionsArray(std::string_view uri,
93                                    const SearchType searchType)
94 {
95     boost::system::result<boost::urls::url> parsedUrl =
96         boost::urls::parse_relative_ref(uri);
97     if (!parsedUrl)
98     {
99         BMCWEB_LOG_ERROR("Failed to get target URI from {}", uri);
100         return false;
101     }
102 
103     parsedUrl->normalize();
104     boost::urls::segments_ref segments = parsedUrl->segments();
105     if (!segments.is_absolute())
106     {
107         return false;
108     }
109 
110     // The passed URI must begin with "/redfish/v1", but we have to strip it
111     // from the URI since topCollections does not include it in its URIs.
112     if (segments.size() < 2)
113     {
114         return false;
115     }
116     if (segments.front() != "redfish")
117     {
118         return false;
119     }
120     segments.erase(segments.begin());
121     if (segments.front() != "v1")
122     {
123         return false;
124     }
125     segments.erase(segments.begin());
126 
127     // Exclude the trailing "/" if it exists such as in "/redfish/v1/".
128     if (!segments.empty() && segments.back().empty())
129     {
130         segments.pop_back();
131     }
132 
133     // If no segments then the passed URI was either "/redfish/v1" or
134     // "/redfish/v1/".
135     if (segments.empty())
136     {
137         return (searchType == SearchType::ContainsSubordinate) ||
138                (searchType == SearchType::CollOrCon);
139     }
140     std::string_view url = segments.buffer();
141     const auto* it = std::ranges::lower_bound(topCollections, url);
142     if (it == topCollections.end())
143     {
144         // parsedUrl is alphabetically after the last entry in the array so it
145         // can't be a top collection or up tree from a top collection
146         return false;
147     }
148 
149     boost::urls::url collectionUrl(*it);
150     boost::urls::segments_view collectionSegments = collectionUrl.segments();
151     boost::urls::segments_view::iterator itCollection =
152         collectionSegments.begin();
153     const boost::urls::segments_view::const_iterator endCollection =
154         collectionSegments.end();
155 
156     // Each segment in the passed URI should match the found collection
157     for (const auto& segment : segments)
158     {
159         if (itCollection == endCollection)
160         {
161             // Leftover segments means the target is for an aggregation
162             // supported resource
163             return searchType == SearchType::Resource;
164         }
165 
166         if (segment != (*itCollection))
167         {
168             return false;
169         }
170         itCollection++;
171     }
172 
173     // No remaining segments means the passed URI was a top level collection
174     if (searchType == SearchType::Collection)
175     {
176         return itCollection == endCollection;
177     }
178     if (searchType == SearchType::ContainsSubordinate)
179     {
180         return itCollection != endCollection;
181     }
182 
183     // Return this check instead of "true" in case other SearchTypes get added
184     return searchType == SearchType::CollOrCon;
185 }
186 
187 // Determines if the passed property contains a URI.  Those property names
188 // either end with a case-insensitive version of "uri" or are specifically
189 // defined in the above array.
isPropertyUri(std::string_view propertyName)190 inline bool isPropertyUri(std::string_view propertyName)
191 {
192     if (propertyName.ends_with("uri") || propertyName.ends_with("Uri") ||
193         propertyName.ends_with("URI"))
194     {
195         return true;
196     }
197     return std::ranges::binary_search(nonUriProperties, propertyName);
198 }
199 
addPrefixToStringItem(std::string & strValue,std::string_view prefix)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::ranges::binary_search(topCollections,
257                                        std::string_view(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 
addPrefixToItem(nlohmann::json & item,std::string_view prefix)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 
addAggregatedHeaders(crow::Response & asyncResp,const crow::Response & resp,std::string_view prefix)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
addPrefixToHeadersInResp(nlohmann::json & json,std::string_view prefix)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.
addPrefixes(nlohmann::json & json,std::string_view prefix)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 
aggregationRetryHandler(unsigned int respCode)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 
getAggregationPolicy()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
constructorCallback(const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)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
findSatelliteConfigs(const dbus::utility::ManagedObjectType & objects,std::unordered_map<std::string,boost::urls::url> & satelliteInfo)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
addSatelliteConfig(const dbus::utility::DBusPropertiesMap & properties,std::unordered_map<std::string,boost::urls::url> & satelliteInfo)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 
startAggregation(AggregationType aggType,const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp) const590     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         std::error_code ec;
612         // Create a filtered copy of the request
613         auto localReq =
614             std::make_shared<crow::Request>(createNewRequest(thisReq));
615         if (ec)
616         {
617             BMCWEB_LOG_ERROR("Failed to create copy of request");
618             if (aggType == AggregationType::Resource)
619             {
620                 messages::internalError(asyncResp->res);
621             }
622             return;
623         }
624 
625         boost::urls::url& urlNew = localReq->url();
626         if (aggType == AggregationType::Collection)
627         {
628             auto paramsIt = urlNew.params().begin();
629             while (paramsIt != urlNew.params().end())
630             {
631                 const boost::urls::param& param = *paramsIt;
632                 // only and $skip, params can't be passed to satellite
633                 // as applying these filters twice results in different results.
634                 // Removing them will cause them to only be processed in the
635                 // aggregator. Note, this still doesn't work for collections
636                 // that might return less than the complete collection by
637                 // default, but hopefully those are rare/nonexistent in top
638                 // collections.  bmcweb doesn't implement any of these.
639                 if (param.key == "only" || param.key == "$skip")
640                 {
641                     BMCWEB_LOG_DEBUG(
642                         "Erasing \"{}\" param from request to top level collection",
643                         param.key);
644 
645                     paramsIt = urlNew.params().erase(paramsIt);
646                     continue;
647                 }
648                 // Pass all other parameters
649                 paramsIt++;
650             }
651         }
652         // Filter headers to only allow Host and Content-Type
653         localReq->target(urlNew.buffer());
654         getSatelliteConfigs(
655             std::bind_front(aggregateAndHandle, aggType, localReq, asyncResp));
656     }
657 
findSatellite(const crow::Request & req,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo,std::string_view memberName)658     static void findSatellite(
659         const crow::Request& req,
660         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
661         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo,
662         std::string_view memberName)
663     {
664         // Determine if the resource ID begins with a known prefix
665         for (const auto& satellite : satelliteInfo)
666         {
667             std::string targetPrefix = satellite.first;
668             targetPrefix += "_";
669             if (memberName.starts_with(targetPrefix))
670             {
671                 BMCWEB_LOG_DEBUG("\"{}\" is a known prefix", satellite.first);
672 
673                 // Remove the known prefix from the request's URI and
674                 // then forward to the associated satellite BMC
675                 getInstance().forwardRequest(req, asyncResp, satellite.first,
676                                              satelliteInfo);
677                 return;
678             }
679         }
680 
681         // We didn't recognize the prefix and need to return a 404
682         std::string nameStr = req.url().segments().back();
683         messages::resourceNotFound(asyncResp->res, "", nameStr);
684     }
685 
686     // Intended to handle an incoming request based on if Redfish Aggregation
687     // is enabled.  Forwards request to satellite BMC if it exists.
aggregateAndHandle(AggregationType aggType,const std::shared_ptr<crow::Request> & sharedReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)688     static void aggregateAndHandle(
689         AggregationType aggType,
690         const std::shared_ptr<crow::Request>& sharedReq,
691         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
692         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
693     {
694         if (sharedReq == nullptr)
695         {
696             return;
697         }
698 
699         // No satellite configs means we don't need to keep attempting to
700         // aggregate
701         if (satelliteInfo.empty())
702         {
703             // For collections or resources that can contain a subordinate
704             // top level collection we'll also handle the request locally so we
705             // don't need to write an error code
706             if (aggType == AggregationType::Resource)
707             {
708                 std::string nameStr = sharedReq->url().segments().back();
709                 messages::resourceNotFound(asyncResp->res, "", nameStr);
710             }
711             return;
712         }
713 
714         const crow::Request& thisReq = *sharedReq;
715         BMCWEB_LOG_DEBUG("Aggregation is enabled, begin processing of {}",
716                          thisReq.target());
717 
718         // We previously determined the request is for a collection.  No need to
719         // check again
720         if (aggType == AggregationType::Collection)
721         {
722             BMCWEB_LOG_DEBUG("Aggregating a collection");
723             // We need to use a specific response handler and send the
724             // request to all known satellites
725             getInstance().forwardCollectionRequests(thisReq, asyncResp,
726                                                     satelliteInfo);
727             return;
728         }
729 
730         // We previously determined the request may contain a subordinate
731         // collection.  No need to check again
732         if (aggType == AggregationType::ContainsSubordinate)
733         {
734             BMCWEB_LOG_DEBUG(
735                 "Aggregating what may have a subordinate collection");
736             // We need to use a specific response handler and send the
737             // request to all known satellites
738             getInstance().forwardContainsSubordinateRequests(thisReq, asyncResp,
739                                                              satelliteInfo);
740             return;
741         }
742 
743         const boost::urls::segments_view urlSegments = thisReq.url().segments();
744         boost::urls::url currentUrl("/");
745         boost::urls::segments_view::const_iterator it = urlSegments.begin();
746         boost::urls::segments_view::const_iterator end = urlSegments.end();
747 
748         // Skip past the leading "/redfish/v1"
749         it++;
750         it++;
751         for (; it != end; it++)
752         {
753             if (std::ranges::binary_search(
754                     topCollections, std::string_view(currentUrl.buffer())))
755             {
756                 // We've matched a resource collection so this current segment
757                 // must contain an aggregation prefix
758                 findSatellite(thisReq, asyncResp, satelliteInfo, *it);
759                 return;
760             }
761 
762             currentUrl.segments().push_back(*it);
763         }
764 
765         // We shouldn't reach this point since we should've hit one of the
766         // previous exits
767         messages::internalError(asyncResp->res);
768     }
769 
770     // Attempt to forward a request to the satellite BMC associated with the
771     // prefix.
forwardRequest(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::string & prefix,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)772     void forwardRequest(
773         const crow::Request& thisReq,
774         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
775         const std::string& prefix,
776         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
777     {
778         const auto& sat = satelliteInfo.find(prefix);
779         if (sat == satelliteInfo.end())
780         {
781             // Realistically this shouldn't get called since we perform an
782             // earlier check to make sure the prefix exists
783             BMCWEB_LOG_ERROR("Unrecognized satellite prefix \"{}\"", prefix);
784             return;
785         }
786 
787         // We need to strip the prefix from the request's path
788         boost::urls::url targetURI(thisReq.target());
789         std::string path = thisReq.url().path();
790         size_t pos = path.find(prefix + "_");
791         if (pos == std::string::npos)
792         {
793             // If this fails then something went wrong
794             BMCWEB_LOG_ERROR("Error removing prefix \"{}_\" from request URI",
795                              prefix);
796             messages::internalError(asyncResp->res);
797             return;
798         }
799         path.erase(pos, prefix.size() + 1);
800 
801         std::function<void(crow::Response&)> cb =
802             std::bind_front(processResponse, prefix, asyncResp);
803 
804         std::string data = thisReq.body();
805         boost::urls::url url(sat->second);
806         url.set_path(path);
807         if (targetURI.has_query())
808         {
809             url.set_query(targetURI.query());
810         }
811 
812         // Prepare request headers
813         boost::beast::http::fields requestFields =
814             prepareAggregationHeaders(thisReq.fields(), prefix);
815 
816         client.sendDataWithCallback(std::move(data), url,
817                                     ensuressl::VerifyCertificate::Verify,
818                                     requestFields, thisReq.method(), cb);
819     }
820 
821     // Forward a request for a collection URI to each known satellite BMC
forwardCollectionRequests(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)822     void forwardCollectionRequests(
823         const crow::Request& thisReq,
824         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
825         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
826     {
827         for (const auto& sat : satelliteInfo)
828         {
829             std::function<void(crow::Response&)> cb = std::bind_front(
830                 processCollectionResponse, sat.first, asyncResp);
831 
832             boost::urls::url url(sat.second);
833             url.set_path(thisReq.url().path());
834             if (thisReq.url().has_query())
835             {
836                 url.set_query(thisReq.url().query());
837             }
838             std::string data = thisReq.body();
839 
840             // Prepare request headers
841             boost::beast::http::fields requestFields =
842                 prepareAggregationHeaders(thisReq.fields(), sat.first);
843 
844             client.sendDataWithCallback(std::move(data), url,
845                                         ensuressl::VerifyCertificate::Verify,
846                                         requestFields, thisReq.method(), cb);
847         }
848     }
849 
850     // Forward request for a URI that is uptree of a top level collection to
851     // each known satellite BMC
forwardContainsSubordinateRequests(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,const std::unordered_map<std::string,boost::urls::url> & satelliteInfo)852     void forwardContainsSubordinateRequests(
853         const crow::Request& thisReq,
854         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
855         const std::unordered_map<std::string, boost::urls::url>& satelliteInfo)
856     {
857         for (const auto& sat : satelliteInfo)
858         {
859             std::function<void(crow::Response&)> cb = std::bind_front(
860                 processContainsSubordinateResponse, sat.first, asyncResp);
861 
862             // will ignore an expanded resource in the response if that resource
863             // is not already supported by the aggregating BMC
864             // TODO: Improve the processing so that we don't have to strip query
865             // params in this specific case
866             boost::urls::url url(sat.second);
867             url.set_path(thisReq.url().path());
868 
869             std::string data = thisReq.body();
870 
871             // Prepare request headers
872             boost::beast::http::fields requestFields =
873                 prepareAggregationHeaders(thisReq.fields(), sat.first);
874 
875             client.sendDataWithCallback(std::move(data), url,
876                                         ensuressl::VerifyCertificate::Verify,
877                                         requestFields, thisReq.method(), cb);
878         }
879     }
880 
881   public:
RedfishAggregator()882     explicit RedfishAggregator() :
883         client(getIoContext(),
884                std::make_shared<crow::ConnectionPolicy>(getAggregationPolicy()))
885     {
886         getSatelliteConfigs(constructorCallback);
887     }
888     RedfishAggregator(const RedfishAggregator&) = delete;
889     RedfishAggregator& operator=(const RedfishAggregator&) = delete;
890     RedfishAggregator(RedfishAggregator&&) = delete;
891     RedfishAggregator& operator=(RedfishAggregator&&) = delete;
892     ~RedfishAggregator() = default;
893 
getInstance()894     static RedfishAggregator& getInstance()
895     {
896         static RedfishAggregator handler;
897         return handler;
898     }
899 
900     // Aggregation sources with their URLs and optional credentials
901     std::unordered_map<std::string, AggregationSource> aggregationSources;
902 
903     // Helper function to prepare headers for aggregated satellite BMC requests
prepareAggregationHeaders(const boost::beast::http::fields & originalFields,const std::string & prefix) const904     boost::beast::http::fields prepareAggregationHeaders(
905         const boost::beast::http::fields& originalFields,
906         const std::string& prefix) const
907     {
908         boost::beast::http::fields fields = originalFields;
909 
910         // POST AggregationService can only parse JSON
911         fields.set(boost::beast::http::field::accept, "application/json");
912 
913         // Add authentication if credentials exist for this prefix
914         auto it = aggregationSources.find(prefix);
915         if (it != aggregationSources.end())
916         {
917             const auto& source = it->second;
918             // Only add auth header if both username and password are provided
919             if (!source.username.empty() && !source.password.empty())
920             {
921                 std::string authHeader = crow::utility::createBasicAuthHeader(
922                     source.username, source.password);
923                 fields.set(boost::beast::http::field::authorization,
924                            authHeader);
925             }
926         }
927         return fields;
928     }
929 
930     // Polls D-Bus to get all available satellite config information
931     // Expects a handler which interacts with the returned configs
getSatelliteConfigs(std::function<void (const std::unordered_map<std::string,boost::urls::url> &)> handler) const932     void getSatelliteConfigs(
933         std::function<
934             void(const std::unordered_map<std::string, boost::urls::url>&)>
935             handler) const
936     {
937         BMCWEB_LOG_DEBUG("Gathering satellite configs");
938 
939         // Extract just the URLs from aggregationSources for the handler
940         std::unordered_map<std::string, boost::urls::url> satelliteInfo;
941         for (const auto& [prefix, source] : aggregationSources)
942         {
943             satelliteInfo.emplace(prefix, source.url);
944         }
945 
946         sdbusplus::message::object_path path("/xyz/openbmc_project/inventory");
947         dbus::utility::getManagedObjects(
948             "xyz.openbmc_project.EntityManager", path,
949             [handler{std::move(handler)},
950              satelliteInfo = std::move(satelliteInfo)](
951                 const boost::system::error_code& ec,
952                 const dbus::utility::ManagedObjectType& objects) mutable {
953                 if (ec)
954                 {
955                     BMCWEB_LOG_WARNING("DBUS response error {}, {}", ec.value(),
956                                        ec.message());
957                 }
958                 else
959                 {
960                     // Maps a chosen alias representing a satellite BMC to a url
961                     // containing the information required to create a http
962                     // connection to the satellite
963                     findSatelliteConfigs(objects, satelliteInfo);
964 
965                     if (!satelliteInfo.empty())
966                     {
967                         BMCWEB_LOG_DEBUG(
968                             "Redfish Aggregation enabled with {} satellite BMCs",
969                             std::to_string(satelliteInfo.size()));
970                     }
971                     else
972                     {
973                         BMCWEB_LOG_DEBUG(
974                             "No satellite BMCs detected.  Redfish Aggregation not enabled");
975                     }
976                 }
977                 handler(satelliteInfo);
978             });
979     }
980 
981     // Processes the response returned by a satellite BMC and loads its
982     // contents into asyncResp
processResponse(std::string_view prefix,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,crow::Response & resp)983     static void processResponse(
984         std::string_view prefix,
985         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
986         crow::Response& resp)
987     {
988         // 429 and 502 mean we didn't actually send the request so don't
989         // overwrite the response headers in that case
990         if ((resp.result() == boost::beast::http::status::too_many_requests) ||
991             (resp.result() == boost::beast::http::status::bad_gateway))
992         {
993             asyncResp->res.result(resp.result());
994             return;
995         }
996 
997         // We want to attempt prefix fixing regardless of response code
998         // The resp will not have a json component
999         // We need to create a json from resp's stringResponse
1000         if (isJsonContentType(resp.getHeaderValue("Content-Type")))
1001         {
1002             nlohmann::json jsonVal =
1003                 nlohmann::json::parse(*resp.body(), nullptr, false);
1004             if (jsonVal.is_discarded())
1005             {
1006                 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON");
1007                 messages::operationFailed(asyncResp->res);
1008                 return;
1009             }
1010 
1011             BMCWEB_LOG_DEBUG("Successfully parsed satellite response");
1012 
1013             addPrefixes(jsonVal, prefix);
1014 
1015             BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response");
1016 
1017             asyncResp->res.result(resp.result());
1018             asyncResp->res.jsonValue = std::move(jsonVal);
1019 
1020             BMCWEB_LOG_DEBUG("Finished writing asyncResp");
1021         }
1022         else
1023         {
1024             // We allow any Content-Type that is not "application/json" now
1025             asyncResp->res.result(resp.result());
1026             asyncResp->res.copyBody(resp);
1027         }
1028         addAggregatedHeaders(asyncResp->res, resp, prefix);
1029     }
1030 
1031     // Processes the collection response returned by a satellite BMC and merges
1032     // its "@odata.id" values
processCollectionResponse(const std::string & prefix,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,crow::Response & resp)1033     static void processCollectionResponse(
1034         const std::string& prefix,
1035         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1036         crow::Response& resp)
1037     {
1038         // 429 and 502 mean we didn't actually send the request so don't
1039         // overwrite the response headers in that case
1040         if ((resp.result() == boost::beast::http::status::too_many_requests) ||
1041             (resp.result() == boost::beast::http::status::bad_gateway))
1042         {
1043             return;
1044         }
1045 
1046         if (resp.resultInt() != 200)
1047         {
1048             BMCWEB_LOG_DEBUG(
1049                 "Collection resource does not exist in satellite BMC \"{}\"",
1050                 prefix);
1051             // Return the error if we haven't had any successes
1052             if (asyncResp->res.resultInt() != 200)
1053             {
1054                 asyncResp->res.result(resp.result());
1055                 asyncResp->res.copyBody(resp);
1056             }
1057             return;
1058         }
1059 
1060         // The resp will not have a json component
1061         // We need to create a json from resp's stringResponse
1062         if (isJsonContentType(resp.getHeaderValue("Content-Type")))
1063         {
1064             nlohmann::json jsonVal =
1065                 nlohmann::json::parse(*resp.body(), nullptr, false);
1066             if (jsonVal.is_discarded())
1067             {
1068                 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON");
1069 
1070                 // Notify the user if doing so won't overwrite a valid response
1071                 if (asyncResp->res.resultInt() != 200)
1072                 {
1073                     messages::operationFailed(asyncResp->res);
1074                 }
1075                 return;
1076             }
1077 
1078             BMCWEB_LOG_DEBUG("Successfully parsed satellite response");
1079 
1080             // Now we need to add the prefix to the URIs contained in the
1081             // response.
1082             addPrefixes(jsonVal, prefix);
1083 
1084             BMCWEB_LOG_DEBUG("Added prefix to parsed satellite response");
1085 
1086             // If this resource collection does not exist on the aggregating bmc
1087             // and has not already been added from processing the response from
1088             // a different satellite then we need to completely overwrite
1089             // asyncResp
1090             if (asyncResp->res.resultInt() != 200)
1091             {
1092                 // We only want to aggregate collections that contain a
1093                 // "Members" array
1094                 if ((!jsonVal.contains("Members")) &&
1095                     (!jsonVal["Members"].is_array()))
1096                 {
1097                     BMCWEB_LOG_DEBUG(
1098                         "Skipping aggregating unsupported resource");
1099                     return;
1100                 }
1101 
1102                 BMCWEB_LOG_DEBUG(
1103                     "Collection does not exist, overwriting asyncResp");
1104                 asyncResp->res.result(resp.result());
1105                 asyncResp->res.jsonValue = std::move(jsonVal);
1106                 asyncResp->res.addHeader("Content-Type", "application/json");
1107 
1108                 BMCWEB_LOG_DEBUG("Finished overwriting asyncResp");
1109             }
1110             else
1111             {
1112                 // We only want to aggregate collections that contain a
1113                 // "Members" array
1114                 if ((!asyncResp->res.jsonValue.contains("Members")) &&
1115                     (!asyncResp->res.jsonValue["Members"].is_array()))
1116 
1117                 {
1118                     BMCWEB_LOG_DEBUG(
1119                         "Skipping aggregating unsupported resource");
1120                     return;
1121                 }
1122 
1123                 BMCWEB_LOG_DEBUG(
1124                     "Adding aggregated resources from \"{}\" to collection",
1125                     prefix);
1126 
1127                 // TODO: This is a potential race condition with multiple
1128                 // satellites and the aggregating bmc attempting to write to
1129                 // update this array.  May need to cascade calls to the next
1130                 // satellite at the end of this function.
1131                 // This is presumably not a concern when there is only a single
1132                 // satellite since the aggregating bmc should have completed
1133                 // before the response is received from the satellite.
1134 
1135                 auto& members = asyncResp->res.jsonValue["Members"];
1136                 auto& satMembers = jsonVal["Members"];
1137                 for (auto& satMem : satMembers)
1138                 {
1139                     members.emplace_back(std::move(satMem));
1140                 }
1141                 asyncResp->res.jsonValue["Members@odata.count"] =
1142                     members.size();
1143 
1144                 // TODO: Do we need to sort() after updating the array?
1145             }
1146         }
1147         else
1148         {
1149             BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"",
1150                              prefix);
1151             // We received a response that was not a json.
1152             // Notify the user only if we did not receive any valid responses
1153             // and if the resource collection does not already exist on the
1154             // aggregating BMC
1155             if (asyncResp->res.resultInt() != 200)
1156             {
1157                 messages::operationFailed(asyncResp->res);
1158             }
1159         }
1160     } // End processCollectionResponse()
1161 
1162     // Processes the response returned by a satellite BMC and merges any
1163     // properties whose "@odata.id" value is the URI or either a top level
1164     // collection or is uptree from a top level collection
processContainsSubordinateResponse(const std::string & prefix,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp,crow::Response & resp)1165     static void processContainsSubordinateResponse(
1166         const std::string& prefix,
1167         const std::shared_ptr<bmcweb::AsyncResp>& asyncResp,
1168         crow::Response& resp)
1169     {
1170         // 429 and 502 mean we didn't actually send the request so don't
1171         // overwrite the response headers in that case
1172         if ((resp.result() == boost::beast::http::status::too_many_requests) ||
1173             (resp.result() == boost::beast::http::status::bad_gateway))
1174         {
1175             return;
1176         }
1177 
1178         if (resp.resultInt() != 200)
1179         {
1180             BMCWEB_LOG_DEBUG(
1181                 "Resource uptree from Collection does not exist in satellite BMC \"{}\"",
1182                 prefix);
1183             // Return the error if we haven't had any successes
1184             if (asyncResp->res.resultInt() != 200)
1185             {
1186                 asyncResp->res.result(resp.result());
1187                 asyncResp->res.copyBody(resp);
1188             }
1189             return;
1190         }
1191 
1192         // The resp will not have a json component
1193         // We need to create a json from resp's stringResponse
1194         if (isJsonContentType(resp.getHeaderValue("Content-Type")))
1195         {
1196             bool addedLinks = false;
1197             nlohmann::json jsonVal =
1198                 nlohmann::json::parse(*resp.body(), nullptr, false);
1199             if (jsonVal.is_discarded())
1200             {
1201                 BMCWEB_LOG_ERROR("Error parsing satellite response as JSON");
1202 
1203                 // Notify the user if doing so won't overwrite a valid response
1204                 if (asyncResp->res.resultInt() != 200)
1205                 {
1206                     messages::operationFailed(asyncResp->res);
1207                 }
1208                 return;
1209             }
1210 
1211             BMCWEB_LOG_DEBUG("Successfully parsed satellite response");
1212 
1213             // Parse response and add properties missing from the AsyncResp
1214             // Valid properties will be of the form <property>.@odata.id and
1215             // @odata.id is a <URI>.  In other words, the json should contain
1216             // multiple properties such that
1217             // {"<property>":{"@odata.id": "<URI>"}}
1218             nlohmann::json::object_t* object =
1219                 jsonVal.get_ptr<nlohmann::json::object_t*>();
1220             if (object == nullptr)
1221             {
1222                 BMCWEB_LOG_ERROR("Parsed JSON was not an object?");
1223                 return;
1224             }
1225 
1226             for (std::pair<const std::string, nlohmann::json>& prop : *object)
1227             {
1228                 if (!prop.second.contains("@odata.id"))
1229                 {
1230                     continue;
1231                 }
1232 
1233                 std::string* strValue =
1234                     prop.second["@odata.id"].get_ptr<std::string*>();
1235                 if (strValue == nullptr)
1236                 {
1237                     BMCWEB_LOG_CRITICAL("Field wasn't a string????");
1238                     continue;
1239                 }
1240                 if (!searchCollectionsArray(*strValue, SearchType::CollOrCon))
1241                 {
1242                     continue;
1243                 }
1244 
1245                 addedLinks = true;
1246                 if (!asyncResp->res.jsonValue.contains(prop.first))
1247                 {
1248                     // Only add the property if it did not already exist
1249                     BMCWEB_LOG_DEBUG("Adding link for {} from BMC {}",
1250                                      *strValue, prefix);
1251                     asyncResp->res.jsonValue[prop.first]["@odata.id"] =
1252                         *strValue;
1253                     continue;
1254                 }
1255             }
1256 
1257             // If we added links to a previously unsuccessful (non-200) response
1258             // then we need to make sure the response contains the bare minimum
1259             // amount of additional information that we'd expect to have been
1260             // populated.
1261             if (addedLinks && (asyncResp->res.resultInt() != 200))
1262             {
1263                 // This resource didn't locally exist or an error
1264                 // occurred while generating the response.  Remove any
1265                 // error messages and update the error code.
1266                 asyncResp->res.jsonValue.erase(
1267                     asyncResp->res.jsonValue.find("error"));
1268                 asyncResp->res.result(resp.result());
1269 
1270                 const auto& it1 = object->find("@odata.id");
1271                 if (it1 != object->end())
1272                 {
1273                     asyncResp->res.jsonValue["@odata.id"] = (it1->second);
1274                 }
1275                 const auto& it2 = object->find("@odata.type");
1276                 if (it2 != object->end())
1277                 {
1278                     asyncResp->res.jsonValue["@odata.type"] = (it2->second);
1279                 }
1280                 const auto& it3 = object->find("Id");
1281                 if (it3 != object->end())
1282                 {
1283                     asyncResp->res.jsonValue["Id"] = (it3->second);
1284                 }
1285                 const auto& it4 = object->find("Name");
1286                 if (it4 != object->end())
1287                 {
1288                     asyncResp->res.jsonValue["Name"] = (it4->second);
1289                 }
1290             }
1291         }
1292         else
1293         {
1294             BMCWEB_LOG_ERROR("Received unparsable response from \"{}\"",
1295                              prefix);
1296             // We received as response that was not a json
1297             // Notify the user only if we did not receive any valid responses,
1298             // and if the resource does not already exist on the aggregating BMC
1299             if (asyncResp->res.resultInt() != 200)
1300             {
1301                 messages::operationFailed(asyncResp->res);
1302             }
1303         }
1304     }
1305 
1306     // Entry point to Redfish Aggregation
1307     // Returns Result stating whether or not we still need to locally handle the
1308     // request
beginAggregation(const crow::Request & thisReq,const std::shared_ptr<bmcweb::AsyncResp> & asyncResp)1309     Result beginAggregation(const crow::Request& thisReq,
1310                             const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
1311     {
1312         using crow::utility::OrMorePaths;
1313         using crow::utility::readUrlSegments;
1314         boost::urls::url_view url = thisReq.url();
1315 
1316         // We don't need to aggregate JsonSchemas due to potential issues such
1317         // as version mismatches between aggregator and satellite BMCs.  For
1318         // now assume that the aggregator has all the schemas and versions that
1319         // the aggregated server has.
1320         if (crow::utility::readUrlSegments(url, "redfish", "v1", "JsonSchemas",
1321                                            crow::utility::OrMorePaths()))
1322         {
1323             return Result::LocalHandle;
1324         }
1325 
1326         // The first two segments should be "/redfish/v1".  We need to check
1327         // that before we can search topCollections
1328         if (!crow::utility::readUrlSegments(url, "redfish", "v1",
1329                                             crow::utility::OrMorePaths()))
1330         {
1331             return Result::LocalHandle;
1332         }
1333 
1334         // Parse the URI to see if it begins with a known top level collection
1335         // such as:
1336         // /redfish/v1/Chassis
1337         // /redfish/v1/UpdateService/FirmwareInventory
1338         const boost::urls::segments_view urlSegments = url.segments();
1339         boost::urls::url currentUrl("/");
1340         boost::urls::segments_view::const_iterator it = urlSegments.begin();
1341         boost::urls::segments_view::const_iterator end = urlSegments.end();
1342 
1343         // Skip past the leading "/redfish/v1"
1344         it++;
1345         it++;
1346         for (; it != end; it++)
1347         {
1348             const std::string& collectionItem = *it;
1349             if (std::ranges::binary_search(
1350                     topCollections, std::string_view(currentUrl.buffer())))
1351             {
1352                 // We've matched a resource collection so this current segment
1353                 // might contain an aggregation prefix
1354                 if (segmentHasPrefix(collectionItem))
1355                 {
1356                     BMCWEB_LOG_DEBUG("Need to forward a request");
1357 
1358                     // Extract the prefix from the request's URI, retrieve the
1359                     // associated satellite config information, and then forward
1360                     // the request to that satellite.
1361                     startAggregation(AggregationType::Resource, thisReq,
1362                                      asyncResp);
1363                     return Result::NoLocalHandle;
1364                 }
1365 
1366                 // Handle collection URI with a trailing backslash
1367                 // e.g. /redfish/v1/Chassis/
1368                 it++;
1369                 if ((it == end) && collectionItem.empty())
1370                 {
1371                     startAggregation(AggregationType::Collection, thisReq,
1372                                      asyncResp);
1373                 }
1374 
1375                 // We didn't recognize the prefix or it's a collection with a
1376                 // trailing "/".  In both cases we still want to locally handle
1377                 // the request
1378                 return Result::LocalHandle;
1379             }
1380 
1381             currentUrl.segments().push_back(collectionItem);
1382         }
1383 
1384         // If we made it here then currentUrl could contain a top level
1385         // collection URI without a trailing "/", e.g. /redfish/v1/Chassis
1386         if (std::ranges::binary_search(topCollections,
1387                                        std::string_view(currentUrl.buffer())))
1388         {
1389             startAggregation(AggregationType::Collection, thisReq, asyncResp);
1390             return Result::LocalHandle;
1391         }
1392 
1393         // If nothing else then the request could be for a resource which has a
1394         // top level collection as a subordinate
1395         if (searchCollectionsArray(url.path(), SearchType::ContainsSubordinate))
1396         {
1397             startAggregation(AggregationType::ContainsSubordinate, thisReq,
1398                              asyncResp);
1399             return Result::LocalHandle;
1400         }
1401 
1402         BMCWEB_LOG_DEBUG("Aggregation not required for {}", url.buffer());
1403         return Result::LocalHandle;
1404     }
1405 
1406     // Check if the given URL segment matches with any satellite prefix
1407     // Assumes the given segment starts with <prefix>_
segmentHasPrefix(const std::string & urlSegment) const1408     bool segmentHasPrefix(const std::string& urlSegment) const
1409     {
1410         // TODO: handle this better
1411         // For now 5B247A_ wont be in the aggregationSources map so
1412         // check explicitly for now
1413         if (urlSegment.starts_with("5B247A_"))
1414         {
1415             return true;
1416         }
1417 
1418         // Find the first underscore
1419         std::size_t underscorePos = urlSegment.find('_');
1420         if (underscorePos == std::string::npos)
1421         {
1422             return false; // No underscore, can't be a satellite prefix
1423         }
1424 
1425         // Extract the prefix
1426         std::string prefix = urlSegment.substr(0, underscorePos);
1427 
1428         // Check if this prefix exists
1429         return aggregationSources.contains(prefix);
1430     }
1431 };
1432 
1433 } // namespace redfish
1434