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