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