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