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