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