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